diff --git a/modules/BlueskyGephi/README.md b/modules/BlueskyGephi/README.md new file mode 100644 index 000000000..bb4757f7b --- /dev/null +++ b/modules/BlueskyGephi/README.md @@ -0,0 +1,39 @@ +# Bluesky Gephi + +This plugin allow you to visualize and explore the network of users in bluesky via the atprotocol. + +# Quick start +- Get a bluesky account +- Generate a password https://bsky.app/settings/app-passwords +- Install the plugin in Gephi +- Open Gephi +- Put handle and password information +- Search for yourself +- Graph of your connection should appears. + +# Docs + +Keep in mind current atproto access point from bluesky is quite permissive and might change in the future. + +## Fetch from user +You can fetch network from user from multiple way : +- Put one or multiple (separated by line return) handles or dids inside the plugin textarea and click on "Go!" +- You can right click on a node and select contextual menu item related to the plugin + - **Bluesky Fetch default data** , will fetch network based on the current configuration on the plugin panel + - **Fetch followers only data**, will fetch only the followers of the node + - **Fetch follows only data**, will fetch only the follows of the node + +## Deep Search +By activating **Fetch also n+1**, the plugin will fetch the selected handles network **and also** the network of the handles found. + +/!\ Keep in mind that this can be very long as some users has a long list of followers or follows. /!\ + +## Crawl Limit +To have the list of the followers and follows of a user, the atproto api is build in a way that the application need to loop over multiple +"pages". It means that for hub user, that have a high number of followers and follows (10k, 100k,1M) it might take an important amount of time +to retrive information for this kind of user. + +Therefore, there is a possibility to limit this by only retriving a fraction of the followers and follows in order to speedup the exploration. + +It's ok to do that if analysing theses hub isn't your main goal, as if theses hub are highly connected, they will automatically appears on the relationship +of other users. \ No newline at end of file diff --git a/modules/BlueskyGephi/pom.xml b/modules/BlueskyGephi/pom.xml new file mode 100644 index 000000000..18da75772 --- /dev/null +++ b/modules/BlueskyGephi/pom.xml @@ -0,0 +1,103 @@ + + + 4.0.0 + + gephi-plugin-parent + org.gephi + 0.10.0 + + + fr.totetmatt + bluesky-gephi + 0.1.0 + nbm + + Bluesky Gephi + + + + com.fasterxml.jackson.core + jackson-databind + 2.13.4.1 + + + org.netbeans.api + org-openide-awt + + + ${project.parent.groupId} + visualization-api + + + ${project.parent.groupId} + datalab-api + + + org.netbeans.api + org-openide-windows + + + org.netbeans.api + org-netbeans-modules-settings + + + org.netbeans.api + org-openide-util-lookup + + + org.netbeans.api + org-openide-util + + + org.gephi + graph-api + + + org.gephi + project-api + + + org.gephi + desktop-project + + + + org.gephi + utils-longtask + + + + + + + org.apache.netbeans.utilities + nbm-maven-plugin + + skip + Apache 2.0 + totetmatt + matthieu.totet@gmail.com + https://totetmatt.fr + https://github.com/totetmatt/gephi-plugins.git + + + + + + + + + + + + oss-sonatype + oss-sonatype + https://oss.sonatype.org/content/repositories/snapshots/ + + true + + + + + + diff --git a/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/BlueskyGephi.java b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/BlueskyGephi.java new file mode 100644 index 000000000..7efadd9f4 --- /dev/null +++ b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/BlueskyGephi.java @@ -0,0 +1,295 @@ +package fr.totetmatt.blueskygephi; + +import fr.totetmatt.blueskygephi.atproto.AtClient; +import fr.totetmatt.blueskygephi.atproto.response.AppBskyGraphGetFollowers; +import fr.totetmatt.blueskygephi.atproto.response.AppBskyGraphGetFollows; +import fr.totetmatt.blueskygephi.atproto.response.AppBskyGraphGetList; +import fr.totetmatt.blueskygephi.atproto.response.common.Identity; +import java.awt.Color; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.logging.Logger; +import java.util.prefs.Preferences; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.gephi.graph.api.Edge; +import org.gephi.graph.api.GraphController; +import org.gephi.graph.api.GraphModel; +import org.gephi.graph.api.Node; +import org.gephi.project.api.Project; +import org.gephi.project.api.ProjectController; +import org.gephi.utils.progress.Progress; +import org.gephi.utils.progress.ProgressTicket; +import org.gephi.utils.progress.ProgressTicketProvider; +import org.openide.util.Exceptions; +import org.openide.util.Lookup; +import org.openide.util.NbPreferences; +import org.openide.util.lookup.ServiceProvider; + +/** + * + * @author totetmatt + */ +@ServiceProvider(service = BlueskyGephi.class) +public class BlueskyGephi { + + protected static final Logger logger = Logger.getLogger(BlueskyGephi.class.getName()); + private final static String NBPREF_BSKY_HANDLE = "bsky.handle"; + private final static String NBPREF_BSKY_PASSWORD = "bsky.password"; + private final static String NBPREF_QUERY = "query"; + private final static String NBPREF_QUERY_ISFOLLOWERSACTIVE = "query.isFollowersActive"; + private final static String NBPREF_QUERY_ISFOLLOWSACTIVE = "query.isFollowsActive"; + private final static String NBPREF_QUERY_ISDEEPSEARCH = "query.isDeepSearch"; + private final static String NBPREF_QUERY_ISLIMITCRAWLACTIVE = "query.isLimitCrawlActive"; + private final static String NBPREF_QUERY_LIMITCRAWL = "query.limitCrawl"; + + private final Preferences nbPref = NbPreferences.forModule(BlueskyGephi.class); + // If ATProto get released and decentralized, this will change to adapt to other instances + final private AtClient client = new AtClient("bsky.social"); + private GraphModel graphModel; + + public BlueskyGephi() { + initProjectAndWorkspace(); + + } + + private void initProjectAndWorkspace() { + ProjectController projectController = Lookup.getDefault().lookup(ProjectController.class); + Project currentProject = projectController.getCurrentProject(); + if (currentProject == null) { + projectController.newProject(); + } + } + + public boolean connect(String handle, String password) { + nbPref.put(NBPREF_BSKY_HANDLE, handle); + nbPref.put(NBPREF_BSKY_PASSWORD, password); + + return client.comAtprotoServerCreateSession(handle, password); + + } + + public String getHandle() { + return nbPref.get(NBPREF_BSKY_HANDLE, ""); + } + + public String getPassword() { + return nbPref.get(NBPREF_BSKY_PASSWORD, ""); + } + + public void setQuery(String query) { + nbPref.put(NBPREF_QUERY, query); + } + + public String getQuery() { + return nbPref.get(NBPREF_QUERY, ""); + } + + public void setIsFollowersActive(boolean isFollowersActive) { + nbPref.putBoolean(NBPREF_QUERY_ISFOLLOWERSACTIVE, isFollowersActive); + } + + public boolean getIsFollowersActive() { + return nbPref.getBoolean(NBPREF_QUERY_ISFOLLOWERSACTIVE, true); + } + + public void setIsFollowsActive(boolean isFollowsActive) { + nbPref.putBoolean(NBPREF_QUERY_ISFOLLOWSACTIVE, isFollowsActive); + } + + public boolean getIsFollowsActive() { + return nbPref.getBoolean(NBPREF_QUERY_ISFOLLOWSACTIVE, true); + } + + public void setIsDeepSearch(boolean setIsDeepSearch) { + nbPref.putBoolean(NBPREF_QUERY_ISDEEPSEARCH, setIsDeepSearch); + } + + public boolean getIsDeepSearch() { + return nbPref.getBoolean(NBPREF_QUERY_ISDEEPSEARCH, true); + } + + public void setIsLimitCrawlActive(boolean isLimitCrawlActive) { + nbPref.putBoolean(NBPREF_QUERY_ISLIMITCRAWLACTIVE, isLimitCrawlActive); + } + + public boolean getIsLimitCrawlActive() { + return nbPref.getBoolean(NBPREF_QUERY_ISLIMITCRAWLACTIVE, true); + } + + public void setLimitCrawl(int limitCrawl) { + nbPref.putInt(NBPREF_QUERY_LIMITCRAWL, limitCrawl); + } + + public int getLimitCrawl() { + return nbPref.getInt(NBPREF_QUERY_LIMITCRAWL, 50); + } + + private Node createNode(Identity i) { + + Node node = graphModel.getGraph().getNode(i.getDid()); + if (node == null) { + node = graphModel.factory().newNode(i.getDid()); + node.setLabel(i.getHandle()); + node.setAttribute("Description", i.getDescription()); + node.setSize(10); + node.setColor(Color.GRAY); + node.setX((float) ((0.01 + Math.random()) * 1000) - 500); + node.setY((float) ((0.01 + Math.random()) * 1000) - 500); + graphModel.getGraph().addNode(node); + } + + return node; + } + + private Edge createEdge(Node source, Node target) { + + Edge edge = graphModel.getGraph().getEdge(source, target); + if (edge == null) { + edge = graphModel.factory().newEdge(source, target, true); + edge.setWeight(1.0); + edge.setColor(Color.GRAY); + graphModel.getGraph().addEdge(edge); + } + + return edge; + } + + private void fetchFollowerFollowsFromActor(String actor, List listInit, boolean isFollowsActive, boolean isFollowersActive, boolean isDeepSearch) { + // To avoid locking Gephi UI + Thread t = new Thread() { + private ProgressTicket progressTicket; + Set foaf = new HashSet<>(); + + private void process(String actor, boolean isDeepSearch, Optional limitCrawl) { + + try { + if (isFollowsActive) { + List responses = client.appBskyGraphGetFollows(actor, limitCrawl); + + for (var response : responses) { + graphModel.getGraph().writeLock(); + Identity subject = response.getSubject(); + Node source = createNode(subject); + source.setColor(Color.GREEN); + for (var follow : response.getFollows()) { + if (isDeepSearch) { + foaf.add(follow.getDid()); + } + Node target = createNode(follow); + createEdge(source, target); + } + graphModel.getGraph().writeUnlock(); + + } + + } + + if (isFollowersActive) { + List responses = client.appBskyGraphGetFollowers(actor, limitCrawl); + + for (var response : responses) { + graphModel.getGraph().writeLock(); + Identity subject = response.getSubject(); + Node target = createNode(subject); + target.setColor(Color.GREEN); + for (var follower : response.getFollowers()) { + if (isDeepSearch) { + foaf.add(follower.getDid()); + } + Node source = createNode(follower); + createEdge(source, target); + } + graphModel.getGraph().writeUnlock(); + } + + } + } catch (Exception e) { + Exceptions.printStackTrace(e); + } finally { + } + } + + @Override + public void run() { + + if (actor != null) { + this.setName("[Bsky] fetching" + actor); + } else { + this.setName("[Bsky] fetching List"); + } + progressTicket = Lookup.getDefault() + .lookup(ProgressTicketProvider.class) + .createTicket(this.getName(), () -> { + interrupt(); + Progress.finish(progressTicket); + return true; + }); + Progress.start(progressTicket); + Progress.switchToIndeterminate(progressTicket); + + if (listInit != null) { + this.foaf.addAll(listInit); + } + if (actor != null) { + process(actor, isDeepSearch, Optional.empty()); + } + if (listInit != null || isDeepSearch) { + Progress.switchToDeterminate(progressTicket, foaf.size()); + for (var foafActor : foaf) { + Progress.setDisplayName(progressTicket, "[Bsky] fetching " + actor + " n+1 > " + foafActor); + if (getIsLimitCrawlActive()) { + process(foafActor, false, Optional.of(getLimitCrawl())); + } else { + process(foafActor, false, Optional.empty()); + } + Progress.progress(progressTicket); + + } + } + Progress.finish(progressTicket); + } + }; + t.start(); + + } + + private Stream manageList(String listId) { + List list = client.appBskyGraphGetList(listId); + return list.stream().flatMap(x -> x.getItems().stream().map(y -> y.getSubject().getDid())); + + } + + private void initGraphTable() { + // Create necessary model for the graph entities + if (!graphModel.getNodeTable().hasColumn("Description")) { + graphModel.getNodeTable().addColumn("Description", String.class); + } + } + + public void fetchFollowerFollowsFromActors(List actors, boolean isFollowsActive, boolean isFollowersActive, boolean isBlocksActive) { + graphModel = Lookup.getDefault().lookup(GraphController.class).getGraphModel(); + initGraphTable(); + actors.stream().forEach(actor -> fetchFollowerFollowsFromActor(actor, null, isFollowsActive, isFollowersActive, getIsDeepSearch())); + } + + public void fetchFollowerFollowsFromActors(List actors) { + graphModel = Lookup.getDefault().lookup(GraphController.class).getGraphModel(); + initGraphTable(); + actors + .stream() + .filter(x -> !x.contains("app.bsky.graph.list")) + .forEach(actor -> fetchFollowerFollowsFromActor(actor, null, getIsFollowsActive(), getIsFollowersActive(), getIsDeepSearch())); + + List listActor = actors + .stream() + .filter(x -> x.contains("app.bsky.graph.list")) + .flatMap(this::manageList) + .collect(Collectors.toList()); + + fetchFollowerFollowsFromActor(null, listActor, getIsFollowsActive(), getIsFollowersActive(), getIsDeepSearch()); + + } +} diff --git a/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/BlueskyGephiMainPanel.form b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/BlueskyGephiMainPanel.form new file mode 100644 index 000000000..42402520a --- /dev/null +++ b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/BlueskyGephiMainPanel.form @@ -0,0 +1,278 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/BlueskyGephiMainPanel.java b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/BlueskyGephiMainPanel.java new file mode 100644 index 000000000..0cb5f9bfd --- /dev/null +++ b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/BlueskyGephiMainPanel.java @@ -0,0 +1,311 @@ +/* + * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license + * Click nbfs://nbhost/SystemFileSystem/Templates/GUIForms/JPanel.java to edit this template + */ +package fr.totetmatt.blueskygephi; + +import java.awt.Color; +import java.awt.Desktop; +import java.io.IOException; +import java.net.URI; +import java.util.Arrays; +import java.util.List; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import org.openide.awt.ActionID; +import org.openide.awt.ActionReference; +import org.openide.util.Exceptions; +import org.openide.util.Lookup; +import org.openide.windows.TopComponent; + +/** + * + * @author totetmatt + */ + +@TopComponent.Description(preferredID = "BlueskyGephiMainPanel", + iconBase = "fr/totetmatt/gephi/twitter/twitterlogo.png" +) + + +@ActionReference(path = "Menu/Window", position = 334) +@TopComponent.OpenActionRegistration(displayName = "Bluesky Gephi", + preferredID = "MainTwitterStreamerWindow") +@ActionID(category = "Window", id = "fr.totetmatt.blueskygephi.BlueskyGephiMainPanel") +@TopComponent.Registration(mode = "layoutmode", openAtStartup = true, position=2) +public class BlueskyGephiMainPanel extends TopComponent { + protected static final Logger consoleLogger = Logger.getLogger(BlueskyGephiMainPanel.class.getName()); + private final BlueskyGephi blueskyGephi; + /** + * Creates new form BlueskyGephiMainPanel + */ + public BlueskyGephiMainPanel() { + initComponents(); + blueskyGephi = Lookup.getDefault().lookup(BlueskyGephi.class); + + credentialsHandleField.setText(blueskyGephi.getHandle()); + credentialsPasswordField.setText(blueskyGephi.getPassword()); + handleSearchTextArea.setText(blueskyGephi.getQuery()); + isFollowersActivated.setSelected(blueskyGephi.getIsFollowersActive()); + isFollowsActivated.setSelected(blueskyGephi.getIsFollowsActive()); + isDeepSearch.setSelected(blueskyGephi.getIsDeepSearch()); + limitCrawlCheckbox.setSelected(blueskyGephi.getIsLimitCrawlActive()); + limitCrawlSpinner.setValue(blueskyGephi.getLimitCrawl()*100); + limitCrawlSpinner.setEnabled(limitCrawlCheckbox.isSelected()); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + credentialsPanel = new javax.swing.JPanel(); + credentialsHandleLabel = new javax.swing.JLabel(); + credentialsHandleField = new javax.swing.JTextField(); + credentialsPasswordLabel = new javax.swing.JLabel(); + credentialsPasswordField = new javax.swing.JPasswordField(); + credentialsConnectButton = new javax.swing.JButton(); + jScrollPane1 = new javax.swing.JScrollPane(); + handleSearchTextArea = new javax.swing.JTextArea(); + fetchLabel = new javax.swing.JLabel(); + isFollowersActivated = new javax.swing.JCheckBox(); + isFollowsActivated = new javax.swing.JCheckBox(); + runFetchButton = new javax.swing.JButton(); + isDeepSearch = new javax.swing.JCheckBox(); + limitCrawlCheckbox = new javax.swing.JCheckBox(); + limitCrawlSpinner = new javax.swing.JSpinner(); + + setToolTipText(org.openide.util.NbBundle.getMessage(BlueskyGephiMainPanel.class, "BlueskyGephiMainPanel.toolTipText")); // NOI18N + setName("Bluesky Gephi"); // NOI18N + + credentialsPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(org.openide.util.NbBundle.getMessage(BlueskyGephiMainPanel.class, "BlueskyGephiMainPanel.credentialsPanel.border.title"))); // NOI18N + credentialsPanel.setToolTipText(org.openide.util.NbBundle.getMessage(BlueskyGephiMainPanel.class, "BlueskyGephiMainPanel.Credentials.toolTipText")); // NOI18N + credentialsPanel.setName("Credentials"); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(credentialsHandleLabel, org.openide.util.NbBundle.getMessage(BlueskyGephiMainPanel.class, "BlueskyGephiMainPanel.credentialsHandleLabel.text")); // NOI18N + + credentialsHandleField.setText(org.openide.util.NbBundle.getMessage(BlueskyGephiMainPanel.class, "BlueskyGephiMainPanel.credentialsHandleField.text")); // NOI18N + + credentialsPasswordLabel.setForeground(new java.awt.Color(0, 51, 255)); + org.openide.awt.Mnemonics.setLocalizedText(credentialsPasswordLabel, org.openide.util.NbBundle.getMessage(BlueskyGephiMainPanel.class, "BlueskyGephiMainPanel.credentialsPasswordLabel.text")); // NOI18N + credentialsPasswordLabel.setCursor(new java.awt.Cursor(java.awt.Cursor.HAND_CURSOR)); + credentialsPasswordLabel.addMouseListener(new java.awt.event.MouseAdapter() { + public void mouseClicked(java.awt.event.MouseEvent evt) { + credentialsPasswordLabelMouseClicked(evt); + } + }); + + credentialsPasswordField.setText(org.openide.util.NbBundle.getMessage(BlueskyGephiMainPanel.class, "BlueskyGephiMainPanel.credentialsPasswordField.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(credentialsConnectButton, org.openide.util.NbBundle.getMessage(BlueskyGephiMainPanel.class, "BlueskyGephiMainPanel.credentialsConnectButton.text")); // NOI18N + credentialsConnectButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + credentialsConnectButtonActionPerformed(evt); + } + }); + + javax.swing.GroupLayout credentialsPanelLayout = new javax.swing.GroupLayout(credentialsPanel); + credentialsPanel.setLayout(credentialsPanelLayout); + credentialsPanelLayout.setHorizontalGroup( + credentialsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(credentialsPanelLayout.createSequentialGroup() + .addContainerGap() + .addGroup(credentialsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(credentialsConnectButton, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(credentialsPanelLayout.createSequentialGroup() + .addGroup(credentialsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(credentialsPasswordLabel) + .addComponent(credentialsHandleLabel)) + .addGap(18, 18, 18) + .addGroup(credentialsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(credentialsHandleField) + .addComponent(credentialsPasswordField)))) + .addContainerGap()) + ); + credentialsPanelLayout.setVerticalGroup( + credentialsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(credentialsPanelLayout.createSequentialGroup() + .addContainerGap() + .addGroup(credentialsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(credentialsHandleLabel) + .addComponent(credentialsHandleField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addGroup(credentialsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(credentialsPasswordLabel) + .addComponent(credentialsPasswordField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(credentialsConnectButton) + .addContainerGap(15, Short.MAX_VALUE)) + ); + + handleSearchTextArea.setColumns(20); + handleSearchTextArea.setRows(5); + jScrollPane1.setViewportView(handleSearchTextArea); + + org.openide.awt.Mnemonics.setLocalizedText(fetchLabel, org.openide.util.NbBundle.getMessage(BlueskyGephiMainPanel.class, "BlueskyGephiMainPanel.fetchLabel.text")); // NOI18N + + isFollowersActivated.setSelected(true); + org.openide.awt.Mnemonics.setLocalizedText(isFollowersActivated, org.openide.util.NbBundle.getMessage(BlueskyGephiMainPanel.class, "BlueskyGephiMainPanel.isFollowersActivated.text")); // NOI18N + isFollowersActivated.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + isFollowersActivatedActionPerformed(evt); + } + }); + + isFollowsActivated.setSelected(true); + org.openide.awt.Mnemonics.setLocalizedText(isFollowsActivated, org.openide.util.NbBundle.getMessage(BlueskyGephiMainPanel.class, "BlueskyGephiMainPanel.isFollowsActivated.text")); // NOI18N + isFollowsActivated.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + isFollowsActivatedActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(runFetchButton, org.openide.util.NbBundle.getMessage(BlueskyGephiMainPanel.class, "BlueskyGephiMainPanel.runFetchButton.text")); // NOI18N + runFetchButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + runFetchButtonActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(isDeepSearch, org.openide.util.NbBundle.getMessage(BlueskyGephiMainPanel.class, "BlueskyGephiMainPanel.isDeepSearch.text")); // NOI18N + isDeepSearch.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + isDeepSearchActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(limitCrawlCheckbox, org.openide.util.NbBundle.getMessage(BlueskyGephiMainPanel.class, "BlueskyGephiMainPanel.limitCrawlCheckbox.text")); // NOI18N + limitCrawlCheckbox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + limitCrawlCheckboxActionPerformed(evt); + } + }); + + limitCrawlSpinner.setToolTipText(org.openide.util.NbBundle.getMessage(BlueskyGephiMainPanel.class, "BlueskyGephiMainPanel.limitCrawlSpinner.toolTipText")); // NOI18N + limitCrawlSpinner.addPropertyChangeListener(new java.beans.PropertyChangeListener() { + public void propertyChange(java.beans.PropertyChangeEvent evt) { + limitCrawlSpinnerPropertyChange(evt); + } + }); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(fetchLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(isFollowersActivated) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(isFollowsActivated)) + .addComponent(isDeepSearch)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) + .addComponent(limitCrawlSpinner) + .addComponent(limitCrawlCheckbox, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))) + .addComponent(runFetchButton, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 353, Short.MAX_VALUE) + .addComponent(credentialsPanel, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addGap(12, 12, 12)) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(credentialsPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(fetchLabel) + .addComponent(isFollowersActivated) + .addComponent(isFollowsActivated) + .addComponent(limitCrawlCheckbox)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(isDeepSearch) + .addComponent(limitCrawlSpinner, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(runFetchButton) + .addContainerGap(51, Short.MAX_VALUE)) + ); + + limitCrawlSpinner.getAccessibleContext().setAccessibleDescription(org.openide.util.NbBundle.getMessage(BlueskyGephiMainPanel.class, "BlueskyGephiMainPanel.limitCrawlSpinner.AccessibleContext.accessibleDescription")); // NOI18N + }// //GEN-END:initComponents + + private void isFollowersActivatedActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_isFollowersActivatedActionPerformed + blueskyGephi.setIsFollowersActive(isFollowersActivated.isSelected()); + }//GEN-LAST:event_isFollowersActivatedActionPerformed + + private void credentialsConnectButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_credentialsConnectButtonActionPerformed + if(blueskyGephi.connect(credentialsHandleField.getText(), String.valueOf(credentialsPasswordField.getPassword()))){ + credentialsConnectButton.setBackground(Color.GREEN); + } else { + credentialsConnectButton.setBackground(Color.RED); + } + }//GEN-LAST:event_credentialsConnectButtonActionPerformed + + private void runFetchButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_runFetchButtonActionPerformed + blueskyGephi.setQuery(handleSearchTextArea.getText()); + List actors = Arrays.asList(handleSearchTextArea.getText().split("\\n")) + .stream() + .map(x -> x.trim()) + .collect(Collectors.toList()); + blueskyGephi.fetchFollowerFollowsFromActors(actors); + + }//GEN-LAST:event_runFetchButtonActionPerformed + + private void isFollowsActivatedActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_isFollowsActivatedActionPerformed + blueskyGephi.setIsFollowsActive(isFollowsActivated.isSelected()); + }//GEN-LAST:event_isFollowsActivatedActionPerformed + + private void credentialsPasswordLabelMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_credentialsPasswordLabelMouseClicked + try { + Desktop.getDesktop().browse(URI.create("https://bsky.app/settings/app-passwords")); // TODO add your handling code here: + } catch (IOException ex) { + Exceptions.printStackTrace(ex); + } + }//GEN-LAST:event_credentialsPasswordLabelMouseClicked + + private void isDeepSearchActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_isDeepSearchActionPerformed + blueskyGephi.setIsDeepSearch(isDeepSearch.isSelected()); + }//GEN-LAST:event_isDeepSearchActionPerformed + + private void limitCrawlCheckboxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_limitCrawlCheckboxActionPerformed + blueskyGephi.setIsLimitCrawlActive(limitCrawlCheckbox.isSelected()); + limitCrawlSpinner.setEnabled(limitCrawlCheckbox.isSelected()); + + }//GEN-LAST:event_limitCrawlCheckboxActionPerformed + + private void limitCrawlSpinnerPropertyChange(java.beans.PropertyChangeEvent evt) {//GEN-FIRST:event_limitCrawlSpinnerPropertyChange + blueskyGephi.setLimitCrawl(Math.max(1,((int)limitCrawlSpinner.getValue())/100)); + }//GEN-LAST:event_limitCrawlSpinnerPropertyChange + + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton credentialsConnectButton; + private javax.swing.JTextField credentialsHandleField; + private javax.swing.JLabel credentialsHandleLabel; + private javax.swing.JPanel credentialsPanel; + private javax.swing.JPasswordField credentialsPasswordField; + private javax.swing.JLabel credentialsPasswordLabel; + private javax.swing.JLabel fetchLabel; + private javax.swing.JTextArea handleSearchTextArea; + private javax.swing.JCheckBox isDeepSearch; + private javax.swing.JCheckBox isFollowersActivated; + private javax.swing.JCheckBox isFollowsActivated; + private javax.swing.JScrollPane jScrollPane1; + private javax.swing.JCheckBox limitCrawlCheckbox; + private javax.swing.JSpinner limitCrawlSpinner; + private javax.swing.JButton runFetchButton; + // End of variables declaration//GEN-END:variables +} diff --git a/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/AtClient.java b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/AtClient.java new file mode 100644 index 000000000..4281f6743 --- /dev/null +++ b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/AtClient.java @@ -0,0 +1,162 @@ +package fr.totetmatt.blueskygephi.atproto; + +/** + * + * @author totetmatt + */ +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import fr.totetmatt.blueskygephi.atproto.response.AppBskyActorGetProfile; +import fr.totetmatt.blueskygephi.atproto.response.AppBskyGraphGetFollowers; +import fr.totetmatt.blueskygephi.atproto.response.AppBskyGraphGetFollows; +import fr.totetmatt.blueskygephi.atproto.response.AppBskyGraphGetList; +import fr.totetmatt.blueskygephi.atproto.response.ComAtprotoServerCreateSession; +import java.io.IOException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import org.openide.util.Exceptions; + +public class AtClient { + + private final ObjectMapper objectMapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + private final AtContext context; + + private final HttpClient client = HttpClient.newHttpClient(); + private ComAtprotoServerCreateSession session = null; +ExecutorService executorService = Executors.newFixedThreadPool(2); + public AtClient(String host) { + context = new AtContext(host); + } + + public boolean comAtprotoServerCreateSession(String identifier, String password) { + try { + HttpRequest request = HttpRequest.newBuilder(context.getURIForLexicon("com.atproto.server.createSession")) + .POST(HttpRequest.BodyPublishers.ofString("{\"identifier\":\"" + identifier + "\",\"password\":\"" + password + "\"}")) + .header("Content-Type", "application/json") + .build(); + var response = client.send(request, HttpResponse.BodyHandlers.ofString()); + this.session = objectMapper.readValue(response.body(), ComAtprotoServerCreateSession.class); + return true; + } catch (IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + private HttpRequest getRequest(String xrpcMethod, HashMap params) { + return HttpRequest + .newBuilder(context.getURIForLexicon(xrpcMethod, params)) + .GET() + .header("Authorization", "Bearer " + session.getAccessJwt()) + .build(); + } + + // Yeah, it should be generalized, and async, but it works right now so it's ok. + public List appBskyGraphGetFollowers(String actor, Optional limitCrawl) { + List pagedResponse = new ArrayList<>(); + try { + var params = new HashMap(); + params.put("actor", actor); + params.put("limit", "100"); + int currentCrawlLoop=0; + while (limitCrawl.isEmpty() ||currentCrawlLoop < limitCrawl.get()) { + var request = getRequest("app.bsky.graph.getFollowers", params); + var response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + AppBskyGraphGetFollowers objectResponse = objectMapper.readValue(response.body(), AppBskyGraphGetFollowers.class); + pagedResponse.add(objectResponse); + if (objectResponse.getCursor() == null) { + break; + } + params.put("cursor", objectResponse.getCursor()); + currentCrawlLoop++; + } + return pagedResponse; + } catch (IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + public List appBskyGraphGetFollows(String actor, Optional limitCrawl) { + List pagedResponse = new ArrayList<>(); + try { + var params = new HashMap(); + params.put("actor", actor); + params.put("limit", "100"); + int currentCrawlLoop=0; + while (limitCrawl.isEmpty() || currentCrawlLoop < limitCrawl.get()) { + var request = getRequest("app.bsky.graph.getFollows", params); + var response = client.send(request, HttpResponse.BodyHandlers.ofString()); + var objectResponse = objectMapper.readValue(response.body(), AppBskyGraphGetFollows.class); + pagedResponse.add(objectResponse); + if (objectResponse.getCursor() == null) { + break; + } + params.put("cursor", objectResponse.getCursor()); + currentCrawlLoop++; + } + return pagedResponse; + } catch (IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + public AppBskyActorGetProfile appBskyActorGetProfile(String actor) { + try { + var params = new HashMap(); + params.put("actor", actor); + var request = getRequest("app.bsky.actor.getProfile", params); + var response = client.send(request, HttpResponse.BodyHandlers.ofString()); + System.out.println(response.body()); + return objectMapper.readValue(response.body(), AppBskyActorGetProfile.class); + } catch (IOException | InterruptedException ex) { + Exceptions.printStackTrace(ex); + } + return null; + } + + public List appBskyGraphGetList(String list) { + List lists = new ArrayList<>(); + try { + var params = new HashMap(); + params.put("list", list); + params.put("limit", "100"); + while (true) { + var request = getRequest("app.bsky.graph.getList", params); + var response = client.send(request, HttpResponse.BodyHandlers.ofString()); + var objectResponse = objectMapper.readValue(response.body(), AppBskyGraphGetList.class); + lists.add(objectResponse); + System.out.println(response.body()); + if (objectResponse.getCursor() == null) { + break; + } + + params.put("cursor", objectResponse.getCursor()); + } + } catch (IOException | InterruptedException ex) { + Exceptions.printStackTrace(ex); + } + return lists; + } + + public AppBskyActorGetProfile appBskyActorGetProfiles(String actors) { + try { + var request = HttpRequest + .newBuilder(context.getURIForLexicon("app.bsky.actor.getProfiles", actors)) + .GET() + .header("Authorization", "Bearer " + session.getAccessJwt()) + .build(); + var response = client.send(request, HttpResponse.BodyHandlers.ofString()); + return objectMapper.readValue(response.body(), AppBskyActorGetProfile.class); + } catch (IOException | InterruptedException ex) { + Exceptions.printStackTrace(ex); + } + return null; + } +} diff --git a/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/AtContext.java b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/AtContext.java new file mode 100644 index 000000000..42c70824a --- /dev/null +++ b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/AtContext.java @@ -0,0 +1,44 @@ +package fr.totetmatt.blueskygephi.atproto; + +/** + * + * @author totetmatt + */ +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.stream.Collectors; + +public class AtContext { + + private final String host; + + public AtContext(String host) { + this.host = host; + } + + private String formatUrl(String host, String lexicon) { + return "https://" + host + "/xrpc/" + lexicon; + } + + public URI getURIForLexicon(String lexicon) { + + return URI.create(formatUrl(host, lexicon)); + } + + public URI getURIForLexicon(String lexicon, HashMap parameters) { + + String url_parameters = parameters.entrySet().stream().map(x + -> URLEncoder.encode(x.getKey(), StandardCharsets.UTF_8) + "=" + URLEncoder.encode(x.getValue(), StandardCharsets.UTF_8) + ).collect(Collectors.joining("&")); + + return URI.create(formatUrl(host, lexicon) + "?" + url_parameters); + } + + public URI getURIForLexicon(String lexicon, String parameters) { + + return URI.create(formatUrl(host, lexicon) + "?" + parameters); + } + +} diff --git a/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/AppBskyActorGetProfile.java b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/AppBskyActorGetProfile.java new file mode 100644 index 000000000..791b02e1d --- /dev/null +++ b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/AppBskyActorGetProfile.java @@ -0,0 +1,28 @@ +package fr.totetmatt.blueskygephi.atproto.response; + +/** + * + * @author totetmatt + */ +public class AppBskyActorGetProfile { + + private String did; + private String handle; + + public String getDid() { + return did; + } + + public void setDid(String did) { + this.did = did; + } + + public String getHandle() { + return handle; + } + + public void setHandle(String handle) { + this.handle = handle; + } + +} diff --git a/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/AppBskyGraphGetFollowers.java b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/AppBskyGraphGetFollowers.java new file mode 100644 index 000000000..75e095381 --- /dev/null +++ b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/AppBskyGraphGetFollowers.java @@ -0,0 +1,40 @@ +package fr.totetmatt.blueskygephi.atproto.response; + +/** + * + * @author totetmatt + */ +import fr.totetmatt.blueskygephi.atproto.response.common.Identity; +import java.util.List; + +public class AppBskyGraphGetFollowers { + + private Identity subject; + + public Identity getSubject() { + return subject; + } + + public void setSubject(Identity subject) { + this.subject = subject; + } + + public List getFollowers() { + return followers; + } + + public void setFollowers(List followers) { + this.followers = followers; + } + + public String getCursor() { + return cursor; + } + + public void setCursor(String cursor) { + this.cursor = cursor; + } + + private List followers; + private String cursor; +} diff --git a/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/AppBskyGraphGetFollows.java b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/AppBskyGraphGetFollows.java new file mode 100644 index 000000000..950fba017 --- /dev/null +++ b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/AppBskyGraphGetFollows.java @@ -0,0 +1,40 @@ +package fr.totetmatt.blueskygephi.atproto.response; + +/** + * + * @author totetmatt + */ +import fr.totetmatt.blueskygephi.atproto.response.common.Identity; +import java.util.List; + +public class AppBskyGraphGetFollows { + + private Identity subject; + + public Identity getSubject() { + return subject; + } + + public void setSubject(Identity subject) { + this.subject = subject; + } + + public List getFollows() { + return follows; + } + + public void setFollows(List follows) { + this.follows = follows; + } + + public String getCursor() { + return cursor; + } + + public void setCursor(String cursor) { + this.cursor = cursor; + } + + private List follows; + private String cursor; +} diff --git a/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/AppBskyGraphGetList.java b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/AppBskyGraphGetList.java new file mode 100644 index 000000000..7702d8291 --- /dev/null +++ b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/AppBskyGraphGetList.java @@ -0,0 +1,38 @@ +/* + * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license + * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template + */ +package fr.totetmatt.blueskygephi.atproto.response; + +import fr.totetmatt.blueskygephi.atproto.response.common.Subject; +import java.util.List; + +/** + * + * @author totetmatt + */ +public class AppBskyGraphGetList { + private List items; + private String cursor; + + + public List getItems() { + return items; + } + + public void setItems(List items) { + this.items = items; + } + + public String getCursor() { + return cursor; + } + + public void setCursor(String cursor) { + this.cursor = cursor; + } + + + + +} diff --git a/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/ComAtprotoServerCreateSession.java b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/ComAtprotoServerCreateSession.java new file mode 100644 index 000000000..181032a56 --- /dev/null +++ b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/ComAtprotoServerCreateSession.java @@ -0,0 +1,65 @@ +package fr.totetmatt.blueskygephi.atproto.response; + +/** + * + * @author totetmatt + */ +public class ComAtprotoServerCreateSession { + + private String did; + private String handle; + private String email; + private String accessJwt; + private String refreshJwt; + + public String getDid() { + return did; + } + + public void setDid(String did) { + this.did = did; + } + + public String getHandle() { + return handle; + } + + public void setHandle(String handle) { + this.handle = handle; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getAccessJwt() { + return accessJwt; + } + + public void setAccessJwt(String accessJwt) { + this.accessJwt = accessJwt; + } + + public String getRefreshJwt() { + return refreshJwt; + } + + public void setRefreshJwt(String refreshJwt) { + this.refreshJwt = refreshJwt; + } + + @Override + public String toString() { + return "GetSessionResponse{" + + "did='" + did + '\'' + + ", handle='" + handle + '\'' + + ", email='" + email + '\'' + + ", accessJwt='" + accessJwt + '\'' + + ", refreshJwt='" + refreshJwt + '\'' + + '}'; + } +} diff --git a/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/common/Identity.java b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/common/Identity.java new file mode 100644 index 000000000..dfa3237b9 --- /dev/null +++ b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/common/Identity.java @@ -0,0 +1,66 @@ +package fr.totetmatt.blueskygephi.atproto.response.common; + +/** + * + * @author totetmatt + */ +public class Identity { + + private String did; + private String handle; + private String description; + + private String avatar; + private String indexedAt; + + public String getDid() { + return did; + } + + public void setDid(String did) { + this.did = did; + } + + public String getHandle() { + return handle; + } + + public void setHandle(String handle) { + this.handle = handle; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getAvatar() { + return avatar; + } + + public void setAvatar(String avatar) { + this.avatar = avatar; + } + + public String getIndexedAt() { + return indexedAt; + } + + public void setIndexedAt(String indexedAt) { + this.indexedAt = indexedAt; + } + + @Override + public String toString() { + return "Identity{" + + "did='" + did + '\'' + + ", handle='" + handle + '\'' + + ", description='" + description + '\'' + + ", avatar='" + avatar + '\'' + + ", indexedAt='" + indexedAt + '\'' + + '}'; + } +} diff --git a/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/common/Subject.java b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/common/Subject.java new file mode 100644 index 000000000..e0297632e --- /dev/null +++ b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/atproto/response/common/Subject.java @@ -0,0 +1,22 @@ +/* + * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license + * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template + */ +package fr.totetmatt.blueskygephi.atproto.response.common; + +/** + * + * @author totetmatt + */ +public class Subject { + private Identity subject; + + public Identity getSubject() { + return subject; + } + + public void setSubject(Identity subject) { + this.subject = subject; + } + +} diff --git a/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/graphmanipulator/BlueskyGephiDefaultGraphManipulator.java b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/graphmanipulator/BlueskyGephiDefaultGraphManipulator.java new file mode 100644 index 000000000..50c810343 --- /dev/null +++ b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/graphmanipulator/BlueskyGephiDefaultGraphManipulator.java @@ -0,0 +1,95 @@ +package fr.totetmatt.blueskygephi.graphmanipulator; + +import fr.totetmatt.blueskygephi.BlueskyGephi; +import java.awt.event.KeyEvent; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.swing.Icon; +import org.gephi.datalab.spi.ContextMenuItemManipulator; +import org.gephi.datalab.spi.ManipulatorUI; +import org.gephi.datalab.spi.nodes.NodesManipulator; +import org.gephi.graph.api.Graph; +import org.gephi.graph.api.Node; +import org.gephi.visualization.spi.GraphContextMenuItem; +import org.openide.util.Lookup; +import org.openide.util.lookup.ServiceProvider; + +/** + * + * @author totetmatt + */ +@ServiceProvider(service = GraphContextMenuItem.class) +public class BlueskyGephiDefaultGraphManipulator implements NodesManipulator, GraphContextMenuItem { + + private Node[] nodes; + + @Override + public void setup(Node[] nodes, Node node) { + this.nodes = nodes; + } + + @Override + public ContextMenuItemManipulator[] getSubItems() { + return null; + } + + @Override + public boolean isAvailable() { + return true; + } + + @Override + public Integer getMnemonicKey() { + return KeyEvent.VK_W; + } + + @Override + public void execute() { + List actors = Stream.of(nodes).map(x -> (String) x.getId()).collect(Collectors.toList()); + Lookup.getDefault() + .lookup(BlueskyGephi.class) + .fetchFollowerFollowsFromActors(actors); + } + + @Override + public String getName() { + return "Bluesky Fetch default data"; + } + + @Override + public String getDescription() { + return "Fetch configured network from the selected nodes"; + } + + @Override + public boolean canExecute() { + return true; + } + + @Override + public ManipulatorUI getUI() { + return null; + } + + @Override + public int getType() { + return 200; + } + + @Override + public int getPosition() { + return 200; + } + + @Override + public Icon getIcon() { + return null; + } + + @Override + public void setup(Graph graph, Node[] nodes) { + this.nodes = nodes; + } + +} diff --git a/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/graphmanipulator/BlueskyGephiDefaultGraphManipulatorBuilder.java b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/graphmanipulator/BlueskyGephiDefaultGraphManipulatorBuilder.java new file mode 100644 index 000000000..6ac931f3b --- /dev/null +++ b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/graphmanipulator/BlueskyGephiDefaultGraphManipulatorBuilder.java @@ -0,0 +1,19 @@ +package fr.totetmatt.blueskygephi.graphmanipulator; + +import org.gephi.datalab.spi.nodes.NodesManipulator; +import org.gephi.datalab.spi.nodes.NodesManipulatorBuilder; +import org.openide.util.lookup.ServiceProvider; + +/** + * + * @author totetmatt + */ +@ServiceProvider(service = NodesManipulatorBuilder.class) +public class BlueskyGephiDefaultGraphManipulatorBuilder implements NodesManipulatorBuilder { + + @Override + public NodesManipulator getNodesManipulator() { + return new BlueskyGephiDefaultGraphManipulator(); + } + +} diff --git a/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/graphmanipulator/BlueskyGephiFollowersGraphManipulator.java b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/graphmanipulator/BlueskyGephiFollowersGraphManipulator.java new file mode 100644 index 000000000..f4626484b --- /dev/null +++ b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/graphmanipulator/BlueskyGephiFollowersGraphManipulator.java @@ -0,0 +1,93 @@ +package fr.totetmatt.blueskygephi.graphmanipulator; + +import fr.totetmatt.blueskygephi.BlueskyGephi; +import java.awt.event.KeyEvent; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.swing.Icon; +import org.gephi.datalab.spi.ContextMenuItemManipulator; +import org.gephi.datalab.spi.ManipulatorUI; +import org.gephi.datalab.spi.nodes.NodesManipulator; +import org.gephi.graph.api.Graph; +import org.gephi.graph.api.Node; +import org.gephi.visualization.spi.GraphContextMenuItem; +import org.openide.util.Lookup; + +/** + * + * @author totetmatt + */ +public class BlueskyGephiFollowersGraphManipulator implements NodesManipulator, GraphContextMenuItem { + + private Node[] nodes; + + @Override + public void setup(Node[] nodes, Node node) { + this.nodes = nodes; + } + + @Override + public ContextMenuItemManipulator[] getSubItems() { + return null; + } + + @Override + public boolean isAvailable() { + return true; + } + + @Override + public Integer getMnemonicKey() { + return KeyEvent.VK_C; + } + + @Override + public void execute() { + List actors = Stream.of(nodes).map(x -> (String) x.getId()).collect(Collectors.toList()); + Lookup.getDefault() + .lookup(BlueskyGephi.class) + .fetchFollowerFollowsFromActors(actors, false, true, false); + } + + @Override + public String getName() { + return "Fetch followers only data"; + } + + @Override + public String getDescription() { + return "Fetch only followers network from the selected nodes"; + } + + @Override + public boolean canExecute() { + return true; + } + + @Override + public ManipulatorUI getUI() { + return null; + } + + @Override + public int getType() { + return 200; + } + + @Override + public int getPosition() { + return 200; + } + + @Override + public Icon getIcon() { + return null; + } + + @Override + public void setup(Graph graph, Node[] nodes) { + this.nodes = nodes; + } + +} diff --git a/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/graphmanipulator/BlueskyGephiFollowsGraphManipulator.java b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/graphmanipulator/BlueskyGephiFollowsGraphManipulator.java new file mode 100644 index 000000000..5fda7c5a9 --- /dev/null +++ b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/graphmanipulator/BlueskyGephiFollowsGraphManipulator.java @@ -0,0 +1,93 @@ +package fr.totetmatt.blueskygephi.graphmanipulator; + +import fr.totetmatt.blueskygephi.BlueskyGephi; +import java.awt.event.KeyEvent; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.swing.Icon; +import org.gephi.datalab.spi.ContextMenuItemManipulator; +import org.gephi.datalab.spi.ManipulatorUI; +import org.gephi.datalab.spi.nodes.NodesManipulator; +import org.gephi.graph.api.Graph; +import org.gephi.graph.api.Node; +import org.gephi.visualization.spi.GraphContextMenuItem; +import org.openide.util.Lookup; + +/** + * + * @author totetmatt + */ +public class BlueskyGephiFollowsGraphManipulator implements NodesManipulator, GraphContextMenuItem { + + private Node[] nodes; + + @Override + public void setup(Node[] nodes, Node node) { + this.nodes = nodes; + } + + @Override + public ContextMenuItemManipulator[] getSubItems() { + return null; + } + + @Override + public boolean isAvailable() { + return true; + } + + @Override + public Integer getMnemonicKey() { + return KeyEvent.VK_X; + } + + @Override + public void execute() { + List actors = Stream.of(nodes).map(x -> (String) x.getId()).collect(Collectors.toList()); + Lookup.getDefault() + .lookup(BlueskyGephi.class) + .fetchFollowerFollowsFromActors(actors, true, false, false); + } + + @Override + public String getName() { + return "Fetch follows only data"; + } + + @Override + public String getDescription() { + return "Fetch only follows network from the selected nodes"; + } + + @Override + public boolean canExecute() { + return true; + } + + @Override + public ManipulatorUI getUI() { + return null; + } + + @Override + public int getType() { + return 200; + } + + @Override + public int getPosition() { + return 200; + } + + @Override + public Icon getIcon() { + return null; + } + + @Override + public void setup(Graph graph, Node[] nodes) { + this.nodes = nodes; + } + +} diff --git a/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/graphmanipulator/BlueskyGephiMainGraphManipulator.java b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/graphmanipulator/BlueskyGephiMainGraphManipulator.java new file mode 100644 index 000000000..ca50ecb82 --- /dev/null +++ b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/graphmanipulator/BlueskyGephiMainGraphManipulator.java @@ -0,0 +1,88 @@ +package fr.totetmatt.blueskygephi.graphmanipulator; + +import java.awt.event.KeyEvent; +import javax.swing.Icon; +import org.gephi.datalab.spi.ContextMenuItemManipulator; +import org.gephi.datalab.spi.ManipulatorUI; +import org.gephi.datalab.spi.nodes.NodesManipulator; +import org.gephi.graph.api.Graph; +import org.gephi.graph.api.Node; +import org.gephi.visualization.spi.GraphContextMenuItem; +import org.openide.util.lookup.ServiceProvider; + +/** + * + * @author totetmatt + */ +@ServiceProvider(service = GraphContextMenuItem.class) +public class BlueskyGephiMainGraphManipulator implements NodesManipulator, GraphContextMenuItem { + + @Override + public void setup(Node[] nodes, Node node) { + + } + + @Override + public ContextMenuItemManipulator[] getSubItems() { + return new ContextMenuItemManipulator[]{ + new BlueskyGephiFollowsGraphManipulator(), + new BlueskyGephiFollowersGraphManipulator() + }; + } + + @Override + public boolean isAvailable() { + return true; + } + + @Override + public Integer getMnemonicKey() { + return KeyEvent.VK_W; + } + + @Override + public void execute() { + + } + + @Override + public String getName() { + return "Bluesky"; + } + + @Override + public String getDescription() { + return "Fetch network from the selected nodes"; + } + + @Override + public boolean canExecute() { + return true; + } + + @Override + public ManipulatorUI getUI() { + return null; + } + + @Override + public int getType() { + return 200; + } + + @Override + public int getPosition() { + return 200; + } + + @Override + public Icon getIcon() { + return null; + } + + @Override + public void setup(Graph graph, Node[] nodes) { + + } + +} diff --git a/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/graphmanipulator/BlueskyGephiMainGraphManipulatorBuilder.java b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/graphmanipulator/BlueskyGephiMainGraphManipulatorBuilder.java new file mode 100644 index 000000000..04fd713f1 --- /dev/null +++ b/modules/BlueskyGephi/src/main/java/fr/totetmatt/blueskygephi/graphmanipulator/BlueskyGephiMainGraphManipulatorBuilder.java @@ -0,0 +1,19 @@ +package fr.totetmatt.blueskygephi.graphmanipulator; + +import org.gephi.datalab.spi.nodes.NodesManipulator; +import org.gephi.datalab.spi.nodes.NodesManipulatorBuilder; +import org.openide.util.lookup.ServiceProvider; + +/** + * + * @author totetmatt + */ +@ServiceProvider(service = NodesManipulatorBuilder.class) +public class BlueskyGephiMainGraphManipulatorBuilder implements NodesManipulatorBuilder { + + @Override + public NodesManipulator getNodesManipulator() { + return new BlueskyGephiMainGraphManipulator(); + } + +} diff --git a/modules/BlueskyGephi/src/main/nbm/manifest.mf b/modules/BlueskyGephi/src/main/nbm/manifest.mf new file mode 100644 index 000000000..873ac14d0 --- /dev/null +++ b/modules/BlueskyGephi/src/main/nbm/manifest.mf @@ -0,0 +1,5 @@ +Manifest-Version: 1.0 +OpenIDE-Module-Name: Bluesky Gephi +OpenIDE-Module-Short-Description: Import network from Bluesky / AtProtocol +OpenIDE-Module-Long-Description: Import network from Bluesky / AtProtocol +OpenIDE-Module-Display-Category: Import diff --git a/modules/BlueskyGephi/src/main/resources/fr/totetmatt/blueskygephi/Bundle.properties b/modules/BlueskyGephi/src/main/resources/fr/totetmatt/blueskygephi/Bundle.properties new file mode 100644 index 000000000..30123d975 --- /dev/null +++ b/modules/BlueskyGephi/src/main/resources/fr/totetmatt/blueskygephi/Bundle.properties @@ -0,0 +1,16 @@ +BlueskyGephiMainPanel.toolTipText=Bluesky Gephi +BlueskyGephiMainPanel.Credentials.toolTipText=Credentials +BlueskyGephiMainPanel.credentialsPanel.border.title=Credentials +BlueskyGephiMainPanel.credentialsHandleLabel.text=Handle +BlueskyGephiMainPanel.credentialsHandleField.text= +BlueskyGephiMainPanel.credentialsConnectButton.text=Connect +BlueskyGephiMainPanel.runFetchButton.text=Go ! +BlueskyGephiMainPanel.isFollowsActivated.text=Follows +BlueskyGephiMainPanel.isFollowersActivated.text=Followers +BlueskyGephiMainPanel.fetchLabel.text=Fetch +BlueskyGephiMainPanel.credentialsPasswordField.text= +BlueskyGephiMainPanel.credentialsPasswordLabel.text=Password +BlueskyGephiMainPanel.isDeepSearch.text=Fetch also n+1 +BlueskyGephiMainPanel.limitCrawlCheckbox.text=Crawl Limit +BlueskyGephiMainPanel.limitCrawlSpinner.AccessibleContext.accessibleDescription= +BlueskyGephiMainPanel.limitCrawlSpinner.toolTipText=Nb Follower / Following to fetch max on a n+1 crawl diff --git a/pom.xml b/pom.xml index a5a924c08..1ca1569b0 100644 --- a/pom.xml +++ b/pom.xml @@ -79,6 +79,7 @@ modules/OrderedLayout modules/OpenSeadragonPlugin modules/WordCloudPlugin + modules/BlueskyGephi