mirror of
https://gitlab.com/biskuteri-cafe/JKomasto2.git
synced 2024-10-03 02:30:50 +02:00
e6fea4c061
(Before this, JKomasto and sometimes the Mastodon web client would get '411 Record Not Found' when submitting the same text after deleting and redrafting. Presumably the Mastodon server caches both whether an idempotency key was fulfilled and which post it leads to, and for some reason it looks up the second and fails.)
590 lines
14 KiB
Java
590 lines
14 KiB
Java
|
|
import javax.swing.JFrame;
|
|
import javax.swing.JPanel;
|
|
import javax.swing.JButton;
|
|
import javax.swing.JLabel;
|
|
import javax.swing.JTextField;
|
|
import javax.swing.JPasswordField;
|
|
import javax.swing.JTextArea;
|
|
import javax.swing.JCheckBox;
|
|
import javax.swing.JOptionPane;
|
|
import javax.swing.BorderFactory;
|
|
import javax.swing.border.Border;
|
|
import java.awt.BorderLayout;
|
|
import java.awt.FlowLayout;
|
|
import java.awt.Dimension;
|
|
import java.awt.Font;
|
|
import java.awt.Cursor;
|
|
import java.awt.Desktop;
|
|
import java.awt.event.ActionListener;
|
|
import java.awt.event.ActionEvent;
|
|
import java.awt.event.KeyEvent;
|
|
|
|
import cafe.biskuteri.hinoki.Tree;
|
|
import java.net.URI;
|
|
import java.io.IOException;
|
|
|
|
|
|
class
|
|
LoginWindow extends JFrame {
|
|
|
|
private JKomasto
|
|
primaire;
|
|
|
|
private MastodonApi
|
|
api;
|
|
|
|
// - -%- -
|
|
|
|
private LoginComponent
|
|
display;
|
|
|
|
private boolean
|
|
serverContacted = false,
|
|
haveAppCredentials = false,
|
|
haveAccessToken = false,
|
|
haveAccountDetails = false;
|
|
|
|
// ---%-@-%---
|
|
|
|
public void
|
|
updateStatusDisplay()
|
|
{
|
|
char prefix1 = serverContacted ? '✓' : '✕';
|
|
char prefix2 = haveAppCredentials ? '✓' : '✕';
|
|
char prefix3 = haveAccessToken ? '✓' : '✕';
|
|
char prefix4 = haveAccountDetails ? '✓' : '✕';
|
|
|
|
StringBuilder b = new StringBuilder();
|
|
b.append(prefix1 + " Have connection to server\n");
|
|
b.append(prefix2 + " Have app credentials\n");
|
|
b.append(prefix3 + " Have access token\n");
|
|
b.append(prefix4 + " Have account details\n");
|
|
display.setText(b.toString());
|
|
|
|
display.paintImmediately(display.getBounds(null));
|
|
}
|
|
|
|
public void
|
|
useCache()
|
|
{
|
|
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
|
|
try
|
|
{
|
|
api.loadCache();
|
|
haveAppCredentials = true;
|
|
haveAccessToken = true;
|
|
display.setInstanceUrl(api.getInstanceUrl());
|
|
updateStatusDisplay();
|
|
}
|
|
catch (IOException eIo)
|
|
{
|
|
JOptionPane.showMessageDialog(
|
|
this,
|
|
"We couldn't get login details from the cache.."
|
|
+ "\n" + eIo.getClass() + ": " + eIo.getMessage()
|
|
);
|
|
display.setAutoLoginToggled(false);
|
|
}
|
|
display.setCursor(null);
|
|
if (!haveAccessToken) return;
|
|
|
|
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
|
|
api.getAccountDetails(new RequestListener() {
|
|
|
|
public void
|
|
connectionFailed(IOException eIo)
|
|
{
|
|
JOptionPane.showMessageDialog(
|
|
LoginWindow.this,
|
|
"Tried to get account details, failed.."
|
|
+ "\n" + eIo.getMessage()
|
|
);
|
|
}
|
|
|
|
public void
|
|
requestFailed(int httpCode, Tree<String> json)
|
|
{
|
|
JOptionPane.showMessageDialog(
|
|
LoginWindow.this,
|
|
"Tried to get account details, failed.."
|
|
+ "\n" + json.get("error").value
|
|
+ "\n(HTTP error code: " + httpCode + ")"
|
|
);
|
|
}
|
|
|
|
public void
|
|
requestSucceeded(Tree<String> json)
|
|
{
|
|
api.setAccountDetails(json);
|
|
serverContacted = true;
|
|
haveAccountDetails = true;
|
|
updateStatusDisplay();
|
|
}
|
|
|
|
});
|
|
display.setCursor(null);
|
|
if (!haveAccountDetails) return;
|
|
|
|
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
|
|
try
|
|
{
|
|
api.saveToCache();
|
|
}
|
|
catch (IOException eIo)
|
|
{
|
|
JOptionPane.showMessageDialog(
|
|
this,
|
|
"We couldn't save login details into the cache.."
|
|
+ "\n" + eIo.getClass() + ": " + eIo.getMessage()
|
|
);
|
|
}
|
|
display.setCursor(null);
|
|
|
|
primaire.finishedLogin();
|
|
}
|
|
|
|
public void
|
|
useInstanceUrl()
|
|
{
|
|
if (display.isAutoLoginToggled()) { useCache(); return; }
|
|
|
|
String url = display.getInstanceUrl();
|
|
if (url.trim().isEmpty()) {
|
|
// Should we show an error dialog..?
|
|
display.setInstanceUrl("");
|
|
return;
|
|
}
|
|
if (!hasProtocol(url)) {
|
|
url = "https://" + url;
|
|
display.setInstanceUrl(url);
|
|
display.paintImmediately(display.getBounds(null));
|
|
}
|
|
serverContacted = false;
|
|
haveAppCredentials = false;
|
|
haveAccessToken = false;
|
|
haveAccountDetails = false;
|
|
|
|
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
|
|
api.testUrlConnection(url, new RequestListener() {
|
|
|
|
public void
|
|
connectionFailed(IOException eIo)
|
|
{
|
|
JOptionPane.showMessageDialog(
|
|
LoginWindow.this,
|
|
"Tried to connect to URL, failed.."
|
|
+ "\n" + eIo.getClass() + ": " + eIo.getMessage()
|
|
);
|
|
}
|
|
|
|
public void
|
|
requestFailed(int httpCode, Tree<String> response)
|
|
{
|
|
JOptionPane.showMessageDialog(
|
|
LoginWindow.this,
|
|
"Tried to connect to URL, failed.."
|
|
+ "\n" + response.get("body").value
|
|
);
|
|
}
|
|
|
|
public void
|
|
requestSucceeded(Tree<String> response)
|
|
{
|
|
serverContacted = true;
|
|
updateStatusDisplay();
|
|
}
|
|
|
|
});
|
|
display.setCursor(null);
|
|
if (!serverContacted) return;
|
|
|
|
api.setInstanceUrl(url);
|
|
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
|
|
api.getAppCredentials(new RequestListener() {
|
|
|
|
public void
|
|
connectionFailed(IOException eIo)
|
|
{
|
|
JOptionPane.showMessageDialog(
|
|
LoginWindow.this,
|
|
"Tried to get app credentials, failed.."
|
|
+ "\n" + eIo.getMessage()
|
|
);
|
|
}
|
|
|
|
public void
|
|
requestFailed(int httpCode, Tree<String> json)
|
|
{
|
|
JOptionPane.showMessageDialog(
|
|
LoginWindow.this,
|
|
"Tried to get app credentials, failed.."
|
|
+ "\n" + json.get("error").value
|
|
+ "\n(HTTP error code: " + httpCode + ")"
|
|
);
|
|
}
|
|
|
|
public void
|
|
requestSucceeded(Tree<String> json)
|
|
{
|
|
api.setAppCredentials(json);
|
|
haveAppCredentials = true;
|
|
updateStatusDisplay();
|
|
}
|
|
|
|
});
|
|
display.setCursor(null);
|
|
if (!haveAppCredentials) return;
|
|
|
|
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
|
|
|
|
URI uri = api.getAuthorisationURL();
|
|
|
|
final String MESSAGE1 =
|
|
"We will need you to login through a web browser,\n"
|
|
+ "and they will give you an authorisation code\n"
|
|
+ "that you will paste here. Sorry..!";
|
|
|
|
boolean supported =
|
|
Desktop.isDesktopSupported()
|
|
&& Desktop.getDesktop().isSupported(Desktop.Action.BROWSE);
|
|
if (supported)
|
|
{
|
|
JOptionPane.showMessageDialog(
|
|
LoginWindow.this,
|
|
MESSAGE1,
|
|
"Authorisation code renewal",
|
|
JOptionPane.INFORMATION_MESSAGE
|
|
);
|
|
try { Desktop.getDesktop().browse(uri); }
|
|
catch (IOException eIo) { supported = false; }
|
|
}
|
|
if (!supported)
|
|
{
|
|
final String MESSAGE2 =
|
|
"\nWe cannot use Desktop.browse(URI) on your\n"
|
|
+ "computer.. You'll have to open your web\n"
|
|
+ "browser yourself, and copy this URL in.";
|
|
|
|
JTextField field = new JTextField();
|
|
field.setText(uri.toString());
|
|
field.setPreferredSize(new Dimension(120, 32));
|
|
field.selectAll();
|
|
|
|
JOptionPane.showMessageDialog(
|
|
LoginWindow.this,
|
|
new Object[] { MESSAGE1, MESSAGE2, field },
|
|
"Authorisation code renewal",
|
|
JOptionPane.INFORMATION_MESSAGE
|
|
);
|
|
}
|
|
display.receiveAuthorisationCode();
|
|
display.setCursor(null);
|
|
}
|
|
|
|
public void
|
|
useAuthorisationCode()
|
|
{
|
|
String code = display.getAuthorisationCode();
|
|
|
|
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
|
|
api.getAccessToken(code, new RequestListener() {
|
|
|
|
public void
|
|
connectionFailed(IOException eIo)
|
|
{
|
|
JOptionPane.showMessageDialog(
|
|
LoginWindow.this,
|
|
"Tried to get app token, failed.."
|
|
+ "\n" + eIo.getMessage()
|
|
);
|
|
}
|
|
|
|
public void
|
|
requestFailed(int httpCode, Tree<String> json)
|
|
{
|
|
JOptionPane.showMessageDialog(
|
|
LoginWindow.this,
|
|
"Tried to get app token, failed.."
|
|
+ "\n" + json.get("error").value
|
|
+ "\n(HTTP error code: " + httpCode + ")"
|
|
);
|
|
}
|
|
|
|
public void
|
|
requestSucceeded(Tree<String> json)
|
|
{
|
|
api.setAccessToken(json);
|
|
haveAccessToken = true;
|
|
updateStatusDisplay();
|
|
}
|
|
|
|
});
|
|
display.setCursor(null);
|
|
if (!haveAccessToken) return;
|
|
|
|
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
|
|
api.getAccountDetails(new RequestListener() {
|
|
|
|
public void
|
|
connectionFailed(IOException eIo)
|
|
{
|
|
JOptionPane.showMessageDialog(
|
|
LoginWindow.this,
|
|
"Tried to get account details, failed.."
|
|
+ "\n" + eIo.getMessage()
|
|
);
|
|
}
|
|
|
|
public void
|
|
requestFailed(int httpCode, Tree<String> json)
|
|
{
|
|
JOptionPane.showMessageDialog(
|
|
LoginWindow.this,
|
|
"Tried to get account details, failed.."
|
|
+ "\n" + json.get("error").value
|
|
+ "\n(HTTP error code: " + httpCode + ")"
|
|
);
|
|
}
|
|
|
|
public void
|
|
requestSucceeded(Tree<String> json)
|
|
{
|
|
api.setAccountDetails(json);
|
|
haveAccountDetails = true;
|
|
updateStatusDisplay();
|
|
}
|
|
|
|
});
|
|
display.setCursor(null);
|
|
if (!haveAccountDetails) return;
|
|
|
|
display.setCursor(new Cursor(Cursor.WAIT_CURSOR));
|
|
try
|
|
{
|
|
api.saveToCache();
|
|
}
|
|
catch (IOException eIo)
|
|
{
|
|
JOptionPane.showMessageDialog(
|
|
this,
|
|
"We couldn't save login details into the cache.."
|
|
+ "\n" + eIo.getClass() + ": " + eIo.getMessage()
|
|
);
|
|
}
|
|
display.setCursor(null);
|
|
|
|
primaire.finishedLogin();
|
|
}
|
|
|
|
// - -%- -
|
|
|
|
public static boolean
|
|
hasProtocol(String url) { return url.matches("^.+://.*"); }
|
|
|
|
// ---%-@-%---
|
|
|
|
LoginWindow(JKomasto primaire)
|
|
{
|
|
super("JKomasto - Login");
|
|
this.primaire = primaire;
|
|
this.api = primaire.getMastodonApi();
|
|
|
|
setLocationByPlatform(true);
|
|
setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
|
|
|
|
display = new LoginComponent(this);
|
|
updateStatusDisplay();
|
|
|
|
display.setPreferredSize(new Dimension(320, 280));
|
|
setContentPane(display);
|
|
pack();
|
|
|
|
setIconImage(primaire.getProgramIcon());
|
|
}
|
|
|
|
}
|
|
|
|
class
|
|
LoginComponent extends JPanel
|
|
implements ActionListener {
|
|
|
|
private LoginWindow
|
|
primaire;
|
|
|
|
// - -%- -
|
|
|
|
private JTextArea
|
|
statusDisplay;
|
|
|
|
private JTextField
|
|
instanceUrlField,
|
|
authorisationCodeField;
|
|
|
|
private JLabel
|
|
instanceUrlLabel,
|
|
authorisationCodeLabel;
|
|
|
|
private JButton
|
|
instanceUrlButton,
|
|
authorisationCodeButton;
|
|
|
|
private JPanel
|
|
labelArea,
|
|
forField,
|
|
accountsPanel;
|
|
|
|
private JCheckBox
|
|
autoLoginToggle;
|
|
|
|
// ---%-@-%---
|
|
|
|
public String
|
|
getInstanceUrl()
|
|
{
|
|
return instanceUrlField.getText();
|
|
}
|
|
|
|
public void
|
|
setInstanceUrl(String url)
|
|
{
|
|
instanceUrlField.setText(url);
|
|
}
|
|
|
|
public String
|
|
getAuthorisationCode()
|
|
{
|
|
return authorisationCodeField.getText();
|
|
}
|
|
|
|
public void
|
|
setText(String status)
|
|
{
|
|
statusDisplay.setText(status);
|
|
}
|
|
|
|
public void
|
|
setAutoLoginToggled(boolean a) { autoLoginToggle.setSelected(a); }
|
|
|
|
public boolean
|
|
isAutoLoginToggled() { return autoLoginToggle.isSelected(); }
|
|
|
|
public void
|
|
actionPerformed(ActionEvent eA)
|
|
{
|
|
if (eA.getSource() == instanceUrlButton)
|
|
primaire.useInstanceUrl();
|
|
if (eA.getSource() == authorisationCodeButton)
|
|
primaire.useAuthorisationCode();
|
|
}
|
|
|
|
public void
|
|
receiveInstanceUrl()
|
|
{
|
|
labelArea.remove(authorisationCodeLabel);
|
|
forField.remove(authorisationCodeButton);
|
|
accountsPanel.remove(authorisationCodeField);
|
|
labelArea.add(instanceUrlLabel, BorderLayout.NORTH);
|
|
forField.add(instanceUrlButton, BorderLayout.EAST);
|
|
accountsPanel.add(instanceUrlField);
|
|
revalidate();
|
|
}
|
|
|
|
public void
|
|
receiveAuthorisationCode()
|
|
{
|
|
labelArea.remove(instanceUrlLabel);
|
|
forField.remove(instanceUrlButton);
|
|
accountsPanel.remove(instanceUrlField);
|
|
labelArea.add(authorisationCodeLabel, BorderLayout.NORTH);
|
|
forField.add(authorisationCodeButton, BorderLayout.EAST);
|
|
accountsPanel.add(authorisationCodeField);
|
|
revalidate();
|
|
}
|
|
|
|
// ---%-@-%---
|
|
|
|
LoginComponent(LoginWindow primaire)
|
|
{
|
|
this.primaire = primaire;
|
|
|
|
Border b1, b2, bi, bo, be;
|
|
b1 = BorderFactory.createEtchedBorder();
|
|
b2 = BorderFactory.createEmptyBorder(8, 8, 8, 8);
|
|
bi = BorderFactory.createCompoundBorder(b1, b2);
|
|
bo = BorderFactory.createEmptyBorder(8, 8, 8, 8);
|
|
be = BorderFactory.createEmptyBorder();
|
|
|
|
Font f1, f2;
|
|
f1 = new Font("Dialog", Font.PLAIN, 12);
|
|
f2 = new Font("Dialog", Font.PLAIN, 16);
|
|
|
|
// We can't use swing.Box for layout,
|
|
// it's malfunctioning for some reason.
|
|
|
|
instanceUrlField = new JTextField();
|
|
instanceUrlField.setFont(f1);
|
|
String s1 = "Enter an instance URL!";
|
|
instanceUrlLabel = new JLabel(s1);
|
|
instanceUrlLabel.setLabelFor(instanceUrlField);
|
|
instanceUrlLabel.setFont(f1);
|
|
instanceUrlButton = new JButton("Connect");
|
|
instanceUrlButton.setFont(f1);
|
|
instanceUrlButton.setMnemonic(KeyEvent.VK_C);
|
|
instanceUrlButton.addActionListener(this);
|
|
|
|
authorisationCodeField = new JPasswordField();
|
|
authorisationCodeField.setFont(f1);
|
|
String s2 = "Paste the authorisation code!";
|
|
authorisationCodeLabel = new JLabel(s2);
|
|
authorisationCodeLabel.setLabelFor(authorisationCodeField);
|
|
authorisationCodeLabel.setFont(f1);
|
|
authorisationCodeButton = new JButton("Login");
|
|
authorisationCodeButton.setFont(f1);
|
|
authorisationCodeButton.setMnemonic(KeyEvent.VK_L);
|
|
authorisationCodeButton.addActionListener(this);
|
|
|
|
labelArea = new JPanel();
|
|
labelArea.setLayout(new BorderLayout());
|
|
|
|
forField = new JPanel();
|
|
forField.setOpaque(false);
|
|
forField.setLayout(new BorderLayout());
|
|
forField.add(labelArea, BorderLayout.WEST);
|
|
|
|
String s3 = "Automatically login if possible";
|
|
autoLoginToggle = new JCheckBox(s3);
|
|
autoLoginToggle.setMnemonic(KeyEvent.VK_A);
|
|
autoLoginToggle.setBorder(be);
|
|
autoLoginToggle.setFont(f1);
|
|
|
|
JPanel optionsPanel = new JPanel();
|
|
optionsPanel.setOpaque(false);
|
|
optionsPanel.setLayout(new FlowLayout(FlowLayout.LEFT));
|
|
optionsPanel.setBorder(bi);
|
|
optionsPanel.add(autoLoginToggle);
|
|
|
|
accountsPanel = new JPanel();
|
|
accountsPanel.setOpaque(false);
|
|
accountsPanel.setLayout(new BorderLayout(0, 4));
|
|
accountsPanel.add(instanceUrlField);
|
|
accountsPanel.add(forField, BorderLayout.SOUTH);
|
|
|
|
statusDisplay = new JTextArea();
|
|
statusDisplay.setEditable(false);
|
|
statusDisplay.setBackground(null);
|
|
statusDisplay.setBorder(bi);
|
|
statusDisplay.setFont(f2);
|
|
|
|
receiveInstanceUrl();
|
|
|
|
setLayout(new BorderLayout(0, 8));
|
|
add(accountsPanel, BorderLayout.NORTH);
|
|
add(optionsPanel, BorderLayout.CENTER);
|
|
add(statusDisplay, BorderLayout.SOUTH);
|
|
|
|
setBorder(bo);
|
|
}
|
|
|
|
}
|