mirror of
https://github.com/bellingcat/reddit-post-scraping-tool.git
synced 2026-06-12 05:28:29 +03:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e88e1a2d5a | ||
|
|
71b65753cf | ||
|
|
002dd57c0d | ||
|
|
6e9f97c444 | ||
|
|
1ff6d2c9c0 | ||
|
|
b356a6beaa | ||
|
|
618aaa45ba | ||
|
|
f321accfbb | ||
|
|
e2e9228bec | ||
|
|
90e7fefa7f | ||
|
|
64ebdca6ee | ||
|
|
ca0458f328 | ||
|
|
bc10b3020e | ||
|
|
fc0c62a1ee | ||
|
|
151183765b | ||
|
|
210beccce8 | ||
|
|
3a3a0b67dc | ||
|
|
7399683352 | ||
|
|
b536b8245a |
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
@@ -8,7 +8,7 @@ updates:
|
|||||||
- package-ecosystem: "nuget"
|
- package-ecosystem: "nuget"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "daily"
|
interval: "daily"
|
||||||
directory: "Reddit Post Scraping Tool"
|
directory: "RPST GUI"
|
||||||
ignore:
|
ignore:
|
||||||
- dependency-name: "Newtonsoft.Json"
|
- dependency-name: "Newtonsoft.Json"
|
||||||
- package-ecosystem: "pip"
|
- package-ecosystem: "pip"
|
||||||
|
|||||||
@@ -6,6 +6,6 @@ WORKDIR /app
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN pip install --upgrade pip && pip install build && python -m build && pip install dist/*.whl
|
RUN pip install --upgrade pip && pip install .
|
||||||
|
|
||||||
ENTRYPOINT ["reddit_post_scraping_tool"]
|
ENTRYPOINT ["rpst"]
|
||||||
|
|||||||
39
README.md
39
README.md
@@ -1,30 +1,35 @@
|
|||||||
# Reddit Post Scraping Tool
|
# RPST (Reddit Post Scraping Tool)
|
||||||
Given a subreddit name and a keyword, this script will return all posts from a specified listing (default is 'top') that contain the provided keyword.
|
Given a subreddit name and a keyword, this script will return all posts from a specified listing (default is 'top') that contain the provided keyword.
|
||||||
|
|
||||||
[](https://github.com/rly0nheart/reddit-post-scraping-tool/actions/workflows/python-publish.yml) [](https://github.com/rly0nheart/reddit-post-scraping-tool/actions/workflows/codeql.yml)  
|
[](https://github.com/rly0nheart/reddit-post-scraping-tool/actions/workflows/python-publish.yml) [](https://github.com/rly0nheart/reddit-post-scraping-tool/actions/workflows/codeql.yml)  
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
# Features (GUI)
|
|
||||||
- [x] Auto dark mode from 6pm - 6am
|
|
||||||
- [x] Saves results to a JSON
|
|
||||||
- [ ] Other features coming soon...
|
|
||||||
|
|
||||||
# TODO (GUI)
|
|
||||||
- [ ] Make it a stand alone executable
|
# ✅ Features
|
||||||
- [ ] Add manual dark mode option, that will be remembered in all sessions
|
## GUI
|
||||||
|
- [x] Dark mode (Right-click)
|
||||||
|
- [x] Saves results to a JSON (Right-click)
|
||||||
|
- [x] Logs errors to a file
|
||||||
|
|
||||||
|
## CLI
|
||||||
|
- [x] Saves results to a JSON (-j/--json)
|
||||||
|
- [x] Automatically checks for new updates. Notifies user if update were found.
|
||||||
|
|
||||||
|
# 📃 TODO
|
||||||
|
## GUI
|
||||||
|
- [ ] Make it installable with a setup.exe/setup.msi file.
|
||||||
|
- [x] Add manual dark mode option, that will be persistent in all sessions
|
||||||
- [ ] Make it save results to a CSV file
|
- [ ] Make it save results to a CSV file
|
||||||
|
|
||||||
# Wiki
|
# 📖 Wiki
|
||||||
[Refer to the Wiki](https://github.com/rly0nheart/reddit-post-scraping-tool/wiki) for installation instructions, in addition to all other documentation.
|
[Refer to the Wiki](https://github.com/rly0nheart/reddit-post-scraping-tool/wiki) for installation instructions, in addition to all other documentation.
|
||||||
|
|
||||||
# Note
|
# 😁 Donations
|
||||||
> This is one of the projects I am working on, while learning Visual Basic, so the implementation/code may be messed up. If that's the case, please feel free to open a pull request using the available templates. Otherwise, enjoy!
|
If you like `RPST` and would like to show support, you can Buy A Coffee for the developer using the button below
|
||||||
|
|
||||||
# Donations
|
<a href="https://www.buymeacoffee.com/_rly0nheart" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174"></a>
|
||||||
If you like `Reddit Post Scraping Tool` and would like to show support, you can Buy A Coffee for the developer using the button below
|
|
||||||
|
|
||||||
<a href="https://www.buymeacoffee.com/189381184" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174"></a>
|
|
||||||
|
|
||||||
Your support will be much appreciated😊
|
Your support will be much appreciated😊
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
|||||||
# Visual Studio Version 17
|
# Visual Studio Version 17
|
||||||
VisualStudioVersion = 17.4.33213.308
|
VisualStudioVersion = 17.4.33213.308
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "Reddit Post Scraping Tool", "Reddit Post Scraping Tool\Reddit Post Scraping Tool.vbproj", "{46C2541E-6F65-461A-A479-F65D445C36EA}"
|
Project("{778DAE3C-4631-46EA-AA77-85C1314464D9}") = "RPST", "RPST\RPST.vbproj", "{46C2541E-6F65-461A-A479-F65D445C36EA}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()> _
|
<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()> _
|
||||||
Partial Class AboutForm
|
Partial Class AboutBox
|
||||||
Inherits System.Windows.Forms.Form
|
Inherits System.Windows.Forms.Form
|
||||||
|
|
||||||
'Form overrides dispose to clean up the component list.
|
'Form overrides dispose to clean up the component list.
|
||||||
@@ -22,11 +22,11 @@ Partial Class AboutForm
|
|||||||
'Do not modify it using the code editor.
|
'Do not modify it using the code editor.
|
||||||
<System.Diagnostics.DebuggerStepThrough()> _
|
<System.Diagnostics.DebuggerStepThrough()> _
|
||||||
Private Sub InitializeComponent()
|
Private Sub InitializeComponent()
|
||||||
Dim resources As ComponentModel.ComponentResourceManager = New ComponentModel.ComponentResourceManager(GetType(AboutForm))
|
Dim resources As ComponentModel.ComponentResourceManager = New ComponentModel.ComponentResourceManager(GetType(AboutBox))
|
||||||
PictureBox1 = New PictureBox()
|
PictureBox1 = New PictureBox()
|
||||||
LicenseRichTextBox = New RichTextBox()
|
LicenseRichTextBox = New RichTextBox()
|
||||||
Label1 = New Label()
|
LicenseLabel = New Label()
|
||||||
Label2 = New Label()
|
ProgramNameLabel = New Label()
|
||||||
DescriptionLabel = New Label()
|
DescriptionLabel = New Label()
|
||||||
VersionLabel = New Label()
|
VersionLabel = New Label()
|
||||||
WikiLinkLabel = New LinkLabel()
|
WikiLinkLabel = New LinkLabel()
|
||||||
@@ -51,33 +51,37 @@ Partial Class AboutForm
|
|||||||
LicenseRichTextBox.ReadOnly = True
|
LicenseRichTextBox.ReadOnly = True
|
||||||
LicenseRichTextBox.Size = New Size(513, 342)
|
LicenseRichTextBox.Size = New Size(513, 342)
|
||||||
LicenseRichTextBox.TabIndex = 1
|
LicenseRichTextBox.TabIndex = 1
|
||||||
LicenseRichTextBox.Text = ""'
|
LicenseRichTextBox.Text = "License notice"
|
||||||
' Label1
|
|
||||||
'
|
'
|
||||||
Label1.AutoSize = True
|
' LicenseLabel
|
||||||
Label1.Font = New Font("Microsoft JhengHei UI", 9.75F, FontStyle.Regular, GraphicsUnit.Point)
|
'
|
||||||
Label1.Location = New Point(28, 150)
|
LicenseLabel.AutoSize = True
|
||||||
Label1.Name = "Label1"
|
LicenseLabel.Font = New Font("Microsoft JhengHei UI", 9.75F, FontStyle.Regular, GraphicsUnit.Point)
|
||||||
Label1.Size = New Size(52, 17)
|
LicenseLabel.Location = New Point(28, 150)
|
||||||
Label1.TabIndex = 2
|
LicenseLabel.Name = "LicenseLabel"
|
||||||
Label1.Text = "License"'
|
LicenseLabel.Size = New Size(52, 17)
|
||||||
' Label2
|
LicenseLabel.TabIndex = 2
|
||||||
|
LicenseLabel.Text = "License"
|
||||||
|
'
|
||||||
|
' ProgramNameLabel
|
||||||
|
'
|
||||||
|
ProgramNameLabel.AutoSize = True
|
||||||
|
ProgramNameLabel.Font = New Font("Microsoft JhengHei", 14.25F, FontStyle.Bold, GraphicsUnit.Point)
|
||||||
|
ProgramNameLabel.Location = New Point(126, 47)
|
||||||
|
ProgramNameLabel.Name = "ProgramNameLabel"
|
||||||
|
ProgramNameLabel.Size = New Size(66, 24)
|
||||||
|
ProgramNameLabel.TabIndex = 3
|
||||||
|
ProgramNameLabel.Text = "Name"
|
||||||
'
|
'
|
||||||
Label2.AutoSize = True
|
|
||||||
Label2.Font = New Font("Microsoft JhengHei", 14.25F, FontStyle.Bold, GraphicsUnit.Point)
|
|
||||||
Label2.Location = New Point(126, 47)
|
|
||||||
Label2.Name = "Label2"
|
|
||||||
Label2.Size = New Size(246, 24)
|
|
||||||
Label2.TabIndex = 3
|
|
||||||
Label2.Text = "Reddit Post Scraping Tool"'
|
|
||||||
' DescriptionLabel
|
' DescriptionLabel
|
||||||
'
|
'
|
||||||
DescriptionLabel.AutoSize = True
|
DescriptionLabel.AutoSize = True
|
||||||
DescriptionLabel.Font = New Font("Segoe UI", 9F, FontStyle.Regular, GraphicsUnit.Point)
|
DescriptionLabel.Font = New Font("Segoe UI", 9F, FontStyle.Regular, GraphicsUnit.Point)
|
||||||
DescriptionLabel.Location = New Point(126, 81)
|
DescriptionLabel.Location = New Point(126, 81)
|
||||||
DescriptionLabel.Name = "DescriptionLabel"
|
DescriptionLabel.Name = "DescriptionLabel"
|
||||||
DescriptionLabel.Size = New Size(0, 15)
|
DescriptionLabel.Size = New Size(67, 15)
|
||||||
DescriptionLabel.TabIndex = 4
|
DescriptionLabel.TabIndex = 4
|
||||||
|
DescriptionLabel.Text = "Description"
|
||||||
'
|
'
|
||||||
' VersionLabel
|
' VersionLabel
|
||||||
'
|
'
|
||||||
@@ -85,9 +89,10 @@ Partial Class AboutForm
|
|||||||
VersionLabel.Font = New Font("Segoe UI", 9F, FontStyle.Italic, GraphicsUnit.Point)
|
VersionLabel.Font = New Font("Segoe UI", 9F, FontStyle.Italic, GraphicsUnit.Point)
|
||||||
VersionLabel.Location = New Point(500, 54)
|
VersionLabel.Location = New Point(500, 54)
|
||||||
VersionLabel.Name = "VersionLabel"
|
VersionLabel.Name = "VersionLabel"
|
||||||
VersionLabel.Size = New Size(42, 15)
|
VersionLabel.Size = New Size(46, 15)
|
||||||
VersionLabel.TabIndex = 5
|
VersionLabel.TabIndex = 5
|
||||||
VersionLabel.Text = "Label4"'
|
VersionLabel.Text = "Version"
|
||||||
|
'
|
||||||
' WikiLinkLabel
|
' WikiLinkLabel
|
||||||
'
|
'
|
||||||
WikiLinkLabel.AutoSize = True
|
WikiLinkLabel.AutoSize = True
|
||||||
@@ -96,8 +101,9 @@ Partial Class AboutForm
|
|||||||
WikiLinkLabel.Size = New Size(30, 15)
|
WikiLinkLabel.Size = New Size(30, 15)
|
||||||
WikiLinkLabel.TabIndex = 6
|
WikiLinkLabel.TabIndex = 6
|
||||||
WikiLinkLabel.TabStop = True
|
WikiLinkLabel.TabStop = True
|
||||||
WikiLinkLabel.Text = "Wiki"'
|
WikiLinkLabel.Text = "Wiki"
|
||||||
' AboutForm
|
'
|
||||||
|
' AboutBox
|
||||||
'
|
'
|
||||||
AutoScaleDimensions = New SizeF(7F, 15F)
|
AutoScaleDimensions = New SizeF(7F, 15F)
|
||||||
AutoScaleMode = AutoScaleMode.Font
|
AutoScaleMode = AutoScaleMode.Font
|
||||||
@@ -106,18 +112,18 @@ Partial Class AboutForm
|
|||||||
Controls.Add(WikiLinkLabel)
|
Controls.Add(WikiLinkLabel)
|
||||||
Controls.Add(VersionLabel)
|
Controls.Add(VersionLabel)
|
||||||
Controls.Add(DescriptionLabel)
|
Controls.Add(DescriptionLabel)
|
||||||
Controls.Add(Label2)
|
Controls.Add(ProgramNameLabel)
|
||||||
Controls.Add(Label1)
|
Controls.Add(LicenseLabel)
|
||||||
Controls.Add(LicenseRichTextBox)
|
Controls.Add(LicenseRichTextBox)
|
||||||
Controls.Add(PictureBox1)
|
Controls.Add(PictureBox1)
|
||||||
FormBorderStyle = FormBorderStyle.FixedSingle
|
FormBorderStyle = FormBorderStyle.FixedSingle
|
||||||
Icon = CType(resources.GetObject("$this.Icon"), Icon)
|
Icon = CType(resources.GetObject("$this.Icon"), Icon)
|
||||||
MaximizeBox = False
|
MaximizeBox = False
|
||||||
MinimizeBox = False
|
MinimizeBox = False
|
||||||
Name = "AboutForm"
|
Name = "AboutBox"
|
||||||
ShowInTaskbar = False
|
ShowInTaskbar = False
|
||||||
StartPosition = FormStartPosition.CenterScreen
|
StartPosition = FormStartPosition.CenterScreen
|
||||||
Text = "About - Reddit Post Scraping Tool"
|
Text = "About"
|
||||||
CType(PictureBox1, ComponentModel.ISupportInitialize).EndInit()
|
CType(PictureBox1, ComponentModel.ISupportInitialize).EndInit()
|
||||||
ResumeLayout(False)
|
ResumeLayout(False)
|
||||||
PerformLayout()
|
PerformLayout()
|
||||||
@@ -125,8 +131,8 @@ Partial Class AboutForm
|
|||||||
|
|
||||||
Friend WithEvents PictureBox1 As PictureBox
|
Friend WithEvents PictureBox1 As PictureBox
|
||||||
Friend WithEvents LicenseRichTextBox As RichTextBox
|
Friend WithEvents LicenseRichTextBox As RichTextBox
|
||||||
Friend WithEvents Label1 As Label
|
Friend WithEvents LicenseLabel As Label
|
||||||
Friend WithEvents Label2 As Label
|
Friend WithEvents ProgramNameLabel As Label
|
||||||
Friend WithEvents DescriptionLabel As Label
|
Friend WithEvents DescriptionLabel As Label
|
||||||
Friend WithEvents VersionLabel As Label
|
Friend WithEvents VersionLabel As Label
|
||||||
Friend WithEvents WikiLinkLabel As LinkLabel
|
Friend WithEvents WikiLinkLabel As LinkLabel
|
||||||
@@ -1,4 +1,64 @@
|
|||||||
<root>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<root>
|
||||||
|
<!--
|
||||||
|
Microsoft ResX Schema
|
||||||
|
|
||||||
|
Version 2.0
|
||||||
|
|
||||||
|
The primary goals of this format is to allow a simple XML format
|
||||||
|
that is mostly human readable. The generation and parsing of the
|
||||||
|
various data types are done through the TypeConverter classes
|
||||||
|
associated with the data types.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
... ado.net/XML headers & schema ...
|
||||||
|
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||||
|
<resheader name="version">2.0</resheader>
|
||||||
|
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||||
|
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||||
|
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||||
|
<data name="Color1" type="System.Drawing.Color, System.Drawing"">Blue</data>
|
||||||
|
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||||
|
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||||
|
</data>
|
||||||
|
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||||
|
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||||
|
<comment>This is a comment</comment>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
There are any number of "resheader" rows that contain simple
|
||||||
|
name/value pairs.
|
||||||
|
|
||||||
|
Each data row contains a name, and value. The row also contains a
|
||||||
|
type or mimetype. Type corresponds to a .NET class that support
|
||||||
|
text/value conversion through the TypeConverter architecture.
|
||||||
|
Classes that don't support this are serialized and stored with the
|
||||||
|
mimetype set.
|
||||||
|
|
||||||
|
The mimetype is used for serialized objects, and tells the
|
||||||
|
ResXResourceReader how to depersist the object. This is currently not
|
||||||
|
extensible. For a given mimetype the value must be set accordingly:
|
||||||
|
|
||||||
|
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||||
|
that the ResXResourceWriter will generate, however the reader can
|
||||||
|
read any of the formats listed below.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.binary.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.soap.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||||
|
value : The object must be serialized into a byte array
|
||||||
|
: using a System.ComponentModel.TypeConverter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
-->
|
||||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||||
<xsd:element name="root" msdata:IsDataSet="true">
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
@@ -61,7 +121,7 @@
|
|||||||
<data name="PictureBox1.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
<data name="PictureBox1.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||||
<value>
|
<value>
|
||||||
iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAYAAAD0eNT6AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAN
|
iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAYAAAD0eNT6AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAN
|
||||||
0AAADdABEGw9BwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAFPuSURBVHhe7d0J
|
0wAADdMBvdUcagAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAFPuSURBVHhe7d0J
|
||||||
nBxVvTZ+ExbZVPTPX0Svonjd5b0qV0UEARXR60Xvq2yyi4DsskR2QfZNEEE2gbAIAgEEAgQIS4BAQEh6
|
nBxVvTZ+ExbZVPTPX0Svonjd5b0qV0UEARXR60Xvq2yyi4DsskR2QfZNEEE2gbAIAgEEAgQIS4BAQEh6
|
||||||
uqfXmUkySaYHbyCsCXuSep9fJyOdzjMz3dW1nKp++Hy+H+AH6e4659T5nao6dc57PM8TERGRDkODIiIi
|
uqfXmUkySaYHbyCsCXuSep9fJyOdzjMz3dW1nKp++Hy+H+AH6e4659T5nao6dc57PM8TERGRDkODIiIi
|
||||||
km40KCIiIulGgyIiIpJuNCgiIiLpRoMiIiKSbjQoIiIi6UaDIiIikm40KCIiIulGgyIiIpJuNCgiIiLp
|
km40KCIiIulGgyIiIpJuNCgiIiLpRoMiIiKSbjQoIiIi6UaDIiIikm40KCIiIulGgyIiIpJuNCgiIiLp
|
||||||
36
RPST GUI/RPST/AboutBox.vb
Normal file
36
RPST GUI/RPST/AboutBox.vb
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
Public Class AboutBox
|
||||||
|
Public Property LicenseText As String = "MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023-2024 Richard Mwewa
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the ""Software""), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE."
|
||||||
|
|
||||||
|
Private Sub AboutForm_Load(sender As Object, e As EventArgs) Handles MyBase.Load
|
||||||
|
ProgramNameLabel.Text = My.Application.Info.AssemblyName
|
||||||
|
DescriptionLabel.Text = "Given a subreddit name and a keyword,
|
||||||
|
RPST returns all top posts
|
||||||
|
(by default) that contain the specified keyword."
|
||||||
|
VersionLabel.Text = $"v{My.Application.Info.Version}"
|
||||||
|
LicenseRichTextBox.Text = LicenseText
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
Private Sub LinkLabel1_LinkClicked(sender As Object, e As LinkLabelLinkClickedEventArgs) Handles WikiLinkLabel.LinkClicked
|
||||||
|
Shell("cmd /c start https://github.com/bellingcat/reddit-post-scraping-tool/wiki")
|
||||||
|
End Sub
|
||||||
|
End Class
|
||||||
50
RPST GUI/RPST/ApiHandler.vb
Normal file
50
RPST GUI/RPST/ApiHandler.vb
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
Imports System.IO
|
||||||
|
Imports System.Net.Http
|
||||||
|
Imports Newtonsoft.Json
|
||||||
|
Imports Newtonsoft.Json.Linq
|
||||||
|
|
||||||
|
''' <summary>
|
||||||
|
''' Handles requests to Reddit and Github APIs.
|
||||||
|
''' </summary>
|
||||||
|
Public Class ApiHandler
|
||||||
|
Public Property LogFile As String = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RedditPostScrapingTool", "logs", $"debug.log")
|
||||||
|
Public Property Headers As String = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1.2 Safari/605.1.15"
|
||||||
|
Public Property UpdatesEndpoint As String = "https://api.github.com/repos/bellingcat/reddit-post-scraping-tool/releases/latest"
|
||||||
|
|
||||||
|
''' <summary>
|
||||||
|
''' Scrape Reddit data.
|
||||||
|
''' </summary>
|
||||||
|
''' <returns>Json object containing scraped data.</returns>
|
||||||
|
Public Function ScrapeReddit(subreddit As String, listing As String, limit As Integer, timeframe As String) As JObject
|
||||||
|
Dim ApiEndpoint As String = $"https://reddit.com/r/{subreddit}/{listing}.json?limit={limit}&t={timeframe}"
|
||||||
|
Return GetJObjectFromEndpoint(ApiEndpoint)
|
||||||
|
End Function
|
||||||
|
|
||||||
|
''' <summary>
|
||||||
|
''' Gets remote version information from the repository release page.
|
||||||
|
''' </summary>
|
||||||
|
''' <returns>Json object containing update data.</returns>
|
||||||
|
Public Function CheckUpdates() As JObject
|
||||||
|
Return GetJObjectFromEndpoint(UpdatesEndpoint)
|
||||||
|
End Function
|
||||||
|
|
||||||
|
Private Function GetJObjectFromEndpoint(endpoint As String) As JObject
|
||||||
|
Try
|
||||||
|
Using httpClient As New HttpClient()
|
||||||
|
httpClient.DefaultRequestHeaders.Add("User-Agent", headers)
|
||||||
|
Dim response As HttpResponseMessage = httpClient.GetAsync(endpoint).Result
|
||||||
|
If response.IsSuccessStatusCode Then
|
||||||
|
Dim json As String = response.Content.ReadAsStringAsync().Result
|
||||||
|
Dim data As JObject = JsonConvert.DeserializeObject(Of JObject)(json)
|
||||||
|
Return data
|
||||||
|
Else
|
||||||
|
MessageBox.Show(response.ReasonPhrase, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
|
||||||
|
End If
|
||||||
|
End Using
|
||||||
|
Catch ex As Exception
|
||||||
|
My.Computer.FileSystem.WriteAllText(LogFile, $"{DateTime.Now}: {ex}{Environment.NewLine}", True)
|
||||||
|
MessageBox.Show($"{ex.Message}. Please see the debug log '{LogFile}' for more information.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
|
||||||
|
End Try
|
||||||
|
Return New JObject()
|
||||||
|
End Function
|
||||||
|
End Class
|
||||||
67
RPST GUI/RPST/DataGridViewHandler.vb
Normal file
67
RPST GUI/RPST/DataGridViewHandler.vb
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
Imports Newtonsoft.Json.Linq
|
||||||
|
|
||||||
|
Public Class DataGridViewHandler
|
||||||
|
''' <summary>
|
||||||
|
''' Initializes the DataGridView by clearing any existing data and setting up the necessary columns.
|
||||||
|
''' </summary>
|
||||||
|
''' <param name="dataGridView">The DataGridView to be initialized.</param>
|
||||||
|
Public Shared Sub AddColumn(dataGridView As DataGridView)
|
||||||
|
' Clear the Columns and Rows before adding Items to them
|
||||||
|
PostsForm.DataGridViewPosts.Rows.Clear()
|
||||||
|
PostsForm.DataGridViewPosts.Columns.Clear()
|
||||||
|
|
||||||
|
PostsForm.DataGridViewPosts.Columns.Add("PostCount", "Post Number")
|
||||||
|
PostsForm.DataGridViewPosts.Columns.Add("PostAuthor", "Author")
|
||||||
|
PostsForm.DataGridViewPosts.Columns.Add("PostID", "ID")
|
||||||
|
PostsForm.DataGridViewPosts.Columns.Add("PostSubreddit", "Subreddit")
|
||||||
|
PostsForm.DataGridViewPosts.Columns.Add("SubredditVisibility", "Subreddit Visibility")
|
||||||
|
PostsForm.DataGridViewPosts.Columns.Add("PostThumbnail", "Thumbnail")
|
||||||
|
PostsForm.DataGridViewPosts.Columns.Add("PostIsNSFW", "NSFW")
|
||||||
|
PostsForm.DataGridViewPosts.Columns.Add("PostIsGilded", "Gilded")
|
||||||
|
PostsForm.DataGridViewPosts.Columns.Add("PostUpvotes", "Upvotes")
|
||||||
|
PostsForm.DataGridViewPosts.Columns.Add("PostUpvoteRatio", "Upvote Ratio")
|
||||||
|
PostsForm.DataGridViewPosts.Columns.Add("PostDownvotes", "Downvotes")
|
||||||
|
PostsForm.DataGridViewPosts.Columns.Add("PostAwards", "Awards")
|
||||||
|
PostsForm.DataGridViewPosts.Columns.Add("PostTopAward", "Top Award")
|
||||||
|
PostsForm.DataGridViewPosts.Columns.Add("PostIsCrosspostable", "Is Crosspostable?")
|
||||||
|
PostsForm.DataGridViewPosts.Columns.Add("PostScore", "Score")
|
||||||
|
PostsForm.DataGridViewPosts.Columns.Add("PostText", "Text")
|
||||||
|
PostsForm.DataGridViewPosts.Columns.Add("PostCategory", "Category")
|
||||||
|
PostsForm.DataGridViewPosts.Columns.Add("PostDomain", "Domain")
|
||||||
|
PostsForm.DataGridViewPosts.Columns.Add("PostPermalink", "Permalink")
|
||||||
|
PostsForm.DataGridViewPosts.Columns.Add("PostCreatedAt", "Created At")
|
||||||
|
PostsForm.DataGridViewPosts.Columns.Add("PostApprovedAt", "Approved At")
|
||||||
|
PostsForm.DataGridViewPosts.Columns.Add("PostApprovedBy", "Approved By")
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
Public Shared Sub AddRow(dataGridView As DataGridView, post As JObject, postNumber As Integer)
|
||||||
|
''' <summary>
|
||||||
|
''' Adds a row to the DataGridView based on the data from a Reddit post.
|
||||||
|
''' </summary>
|
||||||
|
''' <param name="dataGridView">The DataGridView to which the row will be added.</param>
|
||||||
|
''' <param name="post">A JObject representing the Reddit post.</param>
|
||||||
|
''' <param name="postNumber">The number of the post.</param>
|
||||||
|
PostsForm.DataGridViewPosts.Rows.Add(postNumber,
|
||||||
|
post("data")("author"),
|
||||||
|
post("data")("id"),
|
||||||
|
post("data")("subreddit_name_prefixed"),
|
||||||
|
post("data")("subreddit_type"),
|
||||||
|
post("data")("thumbnail"),
|
||||||
|
post("data")("over_18"),
|
||||||
|
post("data")("gilded"),
|
||||||
|
post("data")("ups"),
|
||||||
|
post("data")("upvote_ratio"),
|
||||||
|
post("data")("downs"),
|
||||||
|
post("data")("total_awards_received"),
|
||||||
|
post("data")("top_awarded_type"),
|
||||||
|
post("data")("is_crosspostable"),
|
||||||
|
post("data")("score"),
|
||||||
|
post("data")("selftext"),
|
||||||
|
post("data")("category"),
|
||||||
|
post("data")("domain"),
|
||||||
|
post("data")("permalink"),
|
||||||
|
post("data")("created"),
|
||||||
|
post("data")("approved_at_utc"),
|
||||||
|
post("data")("approved_by"))
|
||||||
|
End Sub
|
||||||
|
End Class
|
||||||
84
RPST GUI/RPST/DeveloperForm.Designer.vb
generated
Normal file
84
RPST GUI/RPST/DeveloperForm.Designer.vb
generated
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()>
|
||||||
|
Partial Class DeveloperForm
|
||||||
|
Inherits System.Windows.Forms.Form
|
||||||
|
|
||||||
|
'Form overrides dispose to clean up the component list.
|
||||||
|
<System.Diagnostics.DebuggerNonUserCode()>
|
||||||
|
Protected Overrides Sub Dispose(ByVal disposing As Boolean)
|
||||||
|
Try
|
||||||
|
If disposing AndAlso components IsNot Nothing Then
|
||||||
|
components.Dispose()
|
||||||
|
End If
|
||||||
|
Finally
|
||||||
|
MyBase.Dispose(disposing)
|
||||||
|
End Try
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
'Required by the Windows Form Designer
|
||||||
|
Private components As System.ComponentModel.IContainer
|
||||||
|
|
||||||
|
'NOTE: The following procedure is required by the Windows Form Designer
|
||||||
|
'It can be modified using the Windows Form Designer.
|
||||||
|
'Do not modify it using the code editor.
|
||||||
|
<System.Diagnostics.DebuggerStepThrough()>
|
||||||
|
Private Sub InitializeComponent()
|
||||||
|
Dim resources As ComponentModel.ComponentResourceManager = New ComponentModel.ComponentResourceManager(GetType(DeveloperForm))
|
||||||
|
AboutMeLinkLabel = New LinkLabel()
|
||||||
|
BuyMeACoffeeLinkLabel = New LinkLabel()
|
||||||
|
GreetingLabel = New Label()
|
||||||
|
SuspendLayout()
|
||||||
|
'
|
||||||
|
' AboutMeLinkLabel
|
||||||
|
'
|
||||||
|
AboutMeLinkLabel.AutoSize = True
|
||||||
|
AboutMeLinkLabel.BackColor = Color.White
|
||||||
|
AboutMeLinkLabel.Location = New Point(33, 426)
|
||||||
|
AboutMeLinkLabel.Name = "AboutMeLinkLabel"
|
||||||
|
AboutMeLinkLabel.Size = New Size(60, 15)
|
||||||
|
AboutMeLinkLabel.TabIndex = 0
|
||||||
|
AboutMeLinkLabel.TabStop = True
|
||||||
|
AboutMeLinkLabel.Text = "About.me"'
|
||||||
|
' BuyMeACoffeeLinkLabel
|
||||||
|
'
|
||||||
|
BuyMeACoffeeLinkLabel.AutoSize = True
|
||||||
|
BuyMeACoffeeLinkLabel.Location = New Point(33, 451)
|
||||||
|
BuyMeACoffeeLinkLabel.Name = "BuyMeACoffeeLinkLabel"
|
||||||
|
BuyMeACoffeeLinkLabel.Size = New Size(96, 15)
|
||||||
|
BuyMeACoffeeLinkLabel.TabIndex = 1
|
||||||
|
BuyMeACoffeeLinkLabel.TabStop = True
|
||||||
|
BuyMeACoffeeLinkLabel.Text = "Buy Me A Coffee"'
|
||||||
|
' GreetingLabel
|
||||||
|
'
|
||||||
|
GreetingLabel.AutoSize = True
|
||||||
|
GreetingLabel.Font = New Font("Verdana", 27.75F, FontStyle.Bold, GraphicsUnit.Point)
|
||||||
|
GreetingLabel.Location = New Point(62, 22)
|
||||||
|
GreetingLabel.Name = "GreetingLabel"
|
||||||
|
GreetingLabel.Size = New Size(382, 45)
|
||||||
|
GreetingLabel.TabIndex = 3
|
||||||
|
GreetingLabel.Text = "Hello, I'm Ritchie"'
|
||||||
|
' DeveloperForm
|
||||||
|
'
|
||||||
|
AutoScaleDimensions = New SizeF(7F, 15F)
|
||||||
|
AutoScaleMode = AutoScaleMode.Font
|
||||||
|
BackgroundImage = CType(resources.GetObject("$this.BackgroundImage"), Image)
|
||||||
|
ClientSize = New Size(510, 510)
|
||||||
|
Controls.Add(BuyMeACoffeeLinkLabel)
|
||||||
|
Controls.Add(AboutMeLinkLabel)
|
||||||
|
Controls.Add(GreetingLabel)
|
||||||
|
FormBorderStyle = FormBorderStyle.FixedSingle
|
||||||
|
MaximizeBox = False
|
||||||
|
MinimizeBox = False
|
||||||
|
Name = "DeveloperForm"
|
||||||
|
ShowIcon = False
|
||||||
|
ShowInTaskbar = False
|
||||||
|
StartPosition = FormStartPosition.CenterParent
|
||||||
|
Text = "Developer"
|
||||||
|
ResumeLayout(False)
|
||||||
|
PerformLayout()
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
Friend WithEvents AboutMeLinkLabel As LinkLabel
|
||||||
|
Friend WithEvents BuyMeACoffeeLinkLabel As LinkLabel
|
||||||
|
Friend WithEvents PictureBox1 As PictureBox
|
||||||
|
Friend WithEvents GreetingLabel As Label
|
||||||
|
End Class
|
||||||
@@ -12,6 +12,6 @@
|
|||||||
End Sub
|
End Sub
|
||||||
|
|
||||||
Private Sub BuyMeACoffeeLinkLabel_LinkClicked(sender As Object, e As LinkLabelLinkClickedEventArgs) Handles BuyMeACoffeeLinkLabel.LinkClicked
|
Private Sub BuyMeACoffeeLinkLabel_LinkClicked(sender As Object, e As LinkLabelLinkClickedEventArgs) Handles BuyMeACoffeeLinkLabel.LinkClicked
|
||||||
Shell("cmd /c start https://buymeacoffee.com/189381184")
|
Shell("cmd /c start https://buymeacoffee.com/_rly0nheart")
|
||||||
End Sub
|
End Sub
|
||||||
End Class
|
End Class
|
||||||
@@ -33,7 +33,7 @@ Namespace My
|
|||||||
|
|
||||||
<Global.System.Diagnostics.DebuggerStepThroughAttribute()> _
|
<Global.System.Diagnostics.DebuggerStepThroughAttribute()> _
|
||||||
Protected Overrides Sub OnCreateMainForm()
|
Protected Overrides Sub OnCreateMainForm()
|
||||||
Me.MainForm = Global.Reddit_Post_Scraping_Tool.StartForm
|
Me.MainForm = Global.RPST.StartForm
|
||||||
End Sub
|
End Sub
|
||||||
End Class
|
End Class
|
||||||
End Namespace
|
End Namespace
|
||||||
@@ -39,7 +39,7 @@ Namespace My.Resources
|
|||||||
Friend ReadOnly Property ResourceManager() As Global.System.Resources.ResourceManager
|
Friend ReadOnly Property ResourceManager() As Global.System.Resources.ResourceManager
|
||||||
Get
|
Get
|
||||||
If Object.ReferenceEquals(resourceMan, Nothing) Then
|
If Object.ReferenceEquals(resourceMan, Nothing) Then
|
||||||
Dim temp As Global.System.Resources.ResourceManager = New Global.System.Resources.ResourceManager("Reddit_Post_Scraping_Tool.Resources", GetType(Resources).Assembly)
|
Dim temp As Global.System.Resources.ResourceManager = New Global.System.Resources.ResourceManager("RPST.Resources", GetType(Resources).Assembly)
|
||||||
resourceMan = temp
|
resourceMan = temp
|
||||||
End If
|
End If
|
||||||
Return resourceMan
|
Return resourceMan
|
||||||
57
RPST GUI/RPST/PostsForm.Designer.vb
generated
Normal file
57
RPST GUI/RPST/PostsForm.Designer.vb
generated
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()> _
|
||||||
|
Partial Class PostsForm
|
||||||
|
Inherits System.Windows.Forms.Form
|
||||||
|
|
||||||
|
'Form overrides dispose to clean up the component list.
|
||||||
|
<System.Diagnostics.DebuggerNonUserCode()> _
|
||||||
|
Protected Overrides Sub Dispose(ByVal disposing As Boolean)
|
||||||
|
Try
|
||||||
|
If disposing AndAlso components IsNot Nothing Then
|
||||||
|
components.Dispose()
|
||||||
|
End If
|
||||||
|
Finally
|
||||||
|
MyBase.Dispose(disposing)
|
||||||
|
End Try
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
'Required by the Windows Form Designer
|
||||||
|
Private components As System.ComponentModel.IContainer
|
||||||
|
|
||||||
|
'NOTE: The following procedure is required by the Windows Form Designer
|
||||||
|
'It can be modified using the Windows Form Designer.
|
||||||
|
'Do not modify it using the code editor.
|
||||||
|
<System.Diagnostics.DebuggerStepThrough()> _
|
||||||
|
Private Sub InitializeComponent()
|
||||||
|
DataGridViewPosts = New DataGridView()
|
||||||
|
CType(DataGridViewPosts, ComponentModel.ISupportInitialize).BeginInit()
|
||||||
|
SuspendLayout()
|
||||||
|
'
|
||||||
|
' DataGridViewPosts
|
||||||
|
'
|
||||||
|
DataGridViewPosts.BackgroundColor = Color.Gainsboro
|
||||||
|
DataGridViewPosts.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize
|
||||||
|
DataGridViewPosts.Dock = DockStyle.Fill
|
||||||
|
DataGridViewPosts.Location = New Point(0, 0)
|
||||||
|
DataGridViewPosts.Name = "DataGridViewPosts"
|
||||||
|
DataGridViewPosts.ReadOnly = True
|
||||||
|
DataGridViewPosts.RowHeadersVisible = False
|
||||||
|
DataGridViewPosts.RowTemplate.Height = 25
|
||||||
|
DataGridViewPosts.Size = New Size(800, 450)
|
||||||
|
DataGridViewPosts.TabIndex = 3
|
||||||
|
'
|
||||||
|
' PostsForm
|
||||||
|
'
|
||||||
|
AutoScaleDimensions = New SizeF(7F, 15F)
|
||||||
|
AutoScaleMode = AutoScaleMode.Font
|
||||||
|
ClientSize = New Size(800, 450)
|
||||||
|
Controls.Add(DataGridViewPosts)
|
||||||
|
Name = "PostsForm"
|
||||||
|
ShowIcon = False
|
||||||
|
ShowInTaskbar = False
|
||||||
|
Text = "PostsForm"
|
||||||
|
CType(DataGridViewPosts, ComponentModel.ISupportInitialize).EndInit()
|
||||||
|
ResumeLayout(False)
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
Friend WithEvents DataGridViewPosts As DataGridView
|
||||||
|
End Class
|
||||||
29
RPST GUI/RPST/PostsProcessor.vb
Normal file
29
RPST GUI/RPST/PostsProcessor.vb
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
Imports Newtonsoft.Json.Linq
|
||||||
|
|
||||||
|
Public Class PostsProcessor
|
||||||
|
Private ApiHandler As New ApiHandler
|
||||||
|
|
||||||
|
''' <summary>
|
||||||
|
''' Fetches Reddit posts based on the given parameters and returns them as a JObject.
|
||||||
|
''' </summary>
|
||||||
|
''' <param name="subreddit">The subreddit to fetch posts from.</param>
|
||||||
|
''' <param name="listing">The type of listing (e.g., "new", "top", etc.).</param>
|
||||||
|
''' <param name="limit">The maximum number of posts to fetch.</param>
|
||||||
|
''' <param name="timeframe">The timeframe to consider for the posts (e.g., "day", "week", "month", "year", "all").</param>
|
||||||
|
''' <returns>A JObject containing the fetched Reddit posts.</returns>
|
||||||
|
Public Function FetchPosts(subreddit As String, listing As String, limit As Integer, timeframe As String) As JObject
|
||||||
|
Dim posts As JObject = ApiHandler.ScrapeReddit(subreddit, listing, limit, timeframe)
|
||||||
|
Return posts
|
||||||
|
End Function
|
||||||
|
|
||||||
|
''' <summary>
|
||||||
|
''' Checks if the given Reddit post contains the given keyword in its text.
|
||||||
|
''' </summary>
|
||||||
|
''' <param name="post">The Reddit post to check.</param>
|
||||||
|
''' <param name="keyword">The keyword to check for.</param>
|
||||||
|
''' <returns>True if the post contains the keyword, False otherwise.</returns>
|
||||||
|
Public Shared Function PostContainsKeyword(post As JObject, keyword As String) As Boolean
|
||||||
|
Return post("data")("selftext").ToString.ToLower(System.Globalization.CultureInfo.InvariantCulture).Contains(keyword.ToLower(System.Globalization.CultureInfo.InvariantCulture))
|
||||||
|
End Function
|
||||||
|
|
||||||
|
End Class
|
||||||
@@ -3,23 +3,29 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>net6.0-windows</TargetFramework>
|
<TargetFramework>net6.0-windows</TargetFramework>
|
||||||
<StartupObject>Reddit_Post_Scraping_Tool.My.MyApplication</StartupObject>
|
<StartupObject>RPST.My.MyApplication</StartupObject>
|
||||||
<UseWindowsForms>true</UseWindowsForms>
|
<UseWindowsForms>true</UseWindowsForms>
|
||||||
<MyType>WindowsForms</MyType>
|
<MyType>WindowsForms</MyType>
|
||||||
<ApplicationIcon>icon.ico</ApplicationIcon>
|
<ApplicationIcon>icon.ico</ApplicationIcon>
|
||||||
<Company>Richard Mwewa</Company>
|
<Company>Bellingcat</Company>
|
||||||
<Description>Given a subreddit name and a keyword, this program returns all top (by default) posts that contain the specified keyword. </Description>
|
<Description>Given a subreddit name and a keyword, RPST (Reddit Post Scraping Tool) returns all top (by default) posts that contain the specified keyword. </Description>
|
||||||
<Copyright>Copyright (c) 2023 Richard Mwewa. All rights reserved.</Copyright>
|
<Copyright>Copyright (c) 2023-2024 Richard Mwewa</Copyright>
|
||||||
<PackageProjectUrl>https://github.com/bellingcat/reddit-post-scraping-tool</PackageProjectUrl>
|
<PackageProjectUrl>https://github.com/bellingcat/reddit-post-scraping-tool</PackageProjectUrl>
|
||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
<RepositoryUrl>https://github.com/bellingcat/reddit-post-scraping-tool</RepositoryUrl>
|
<RepositoryUrl>https://github.com/bellingcat/reddit-post-scraping-tool</RepositoryUrl>
|
||||||
<AssemblyVersion>1.3.0.1</AssemblyVersion>
|
<AssemblyVersion>1.4.0.0</AssemblyVersion>
|
||||||
<FileVersion>1.3.0.1</FileVersion>
|
<FileVersion>1.4.0.0</FileVersion>
|
||||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||||
<PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>
|
<PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>
|
||||||
<Version>1.3.0</Version>
|
<Version>1.4.0</Version>
|
||||||
<PackageTags>reddit;scraper;reddit-scraper;osint</PackageTags>
|
<PackageTags>reddit;scraper;reddit-scraper;osint</PackageTags>
|
||||||
<PackageReleaseNotes></PackageReleaseNotes>
|
<PackageReleaseNotes></PackageReleaseNotes>
|
||||||
|
<AnalysisLevel>6.0-recommended</AnalysisLevel>
|
||||||
|
<PackageId>RPST (Reddit Post Scraping Tool)</PackageId>
|
||||||
|
<Authors>Richard Mwewa</Authors>
|
||||||
|
<NeutralLanguage>en</NeutralLanguage>
|
||||||
|
<Product>$(AssemblyName)</Product>
|
||||||
|
<AssemblyName>RPST (Reddit Post Scraping Tool)</AssemblyName>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Update="AboutForm.vb">
|
<Compile Update="AboutBox.vb">
|
||||||
<SubType>Form</SubType>
|
<SubType>Form</SubType>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Update="DeveloperForm.vb">
|
<Compile Update="DeveloperForm.vb">
|
||||||
111
RPST GUI/RPST/Settings.vb
Normal file
111
RPST GUI/RPST/Settings.vb
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
Imports Newtonsoft.Json
|
||||||
|
Imports Newtonsoft.Json.Linq
|
||||||
|
Imports System.IO
|
||||||
|
Imports System.Runtime
|
||||||
|
Imports System.Text.Json
|
||||||
|
|
||||||
|
Public Class SettingsManager
|
||||||
|
|
||||||
|
Public Property DarkMode As Boolean
|
||||||
|
Private ReadOnly settingsFilePath As String = Path.Combine(Environment.CurrentDirectory, "settings.json")
|
||||||
|
|
||||||
|
|
||||||
|
' Load settings from the 'settings.json' file
|
||||||
|
Public Sub LoadSettings()
|
||||||
|
' Check if the settings.json file exists
|
||||||
|
' and load the configurations from it
|
||||||
|
If File.Exists(settingsFilePath) Then
|
||||||
|
Dim json As String = File.ReadAllText(settingsFilePath)
|
||||||
|
Dim options As New JsonSerializerOptions With {.PropertyNameCaseInsensitive = True}
|
||||||
|
Dim settings = Text.Json.JsonSerializer.Deserialize(Of SettingsManager)(json, options)
|
||||||
|
Me.DarkMode = settings.DarkMode
|
||||||
|
StartForm.DarkModeToolStripMenuItem.Checked = settings.DarkMode
|
||||||
|
Else
|
||||||
|
' Settings file does not exist
|
||||||
|
' Create a new file with default settings 'False'
|
||||||
|
Dim defaultSettings = New SettingsManager With {.DarkMode = False}
|
||||||
|
Dim jsonOutput = Text.Json.JsonSerializer.Serialize(defaultSettings)
|
||||||
|
File.WriteAllText(settingsFilePath, jsonOutput)
|
||||||
|
|
||||||
|
Me.DarkMode = False
|
||||||
|
StartForm.DarkModeToolStripMenuItem.Checked = False
|
||||||
|
End If
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
|
||||||
|
' Toggle Dark mode
|
||||||
|
Public Sub ToggleDarkMode(enabled As Boolean)
|
||||||
|
Dim json As String = File.ReadAllText(settingsFilePath)
|
||||||
|
Dim options As New JsonSerializerOptions With {.PropertyNameCaseInsensitive = True}
|
||||||
|
Dim settings As SettingsManager = Text.Json.JsonSerializer.Deserialize(Of SettingsManager)(json, options)
|
||||||
|
settings.DarkMode = enabled
|
||||||
|
SaveSettings(settings)
|
||||||
|
ApplyTheme()
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
|
||||||
|
' Save current settings to settings.json
|
||||||
|
Private Sub SaveSettings(settings)
|
||||||
|
Dim jsonOutput = Text.Json.JsonSerializer.Serialize(settings)
|
||||||
|
File.WriteAllText(settingsFilePath, jsonOutput)
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
|
||||||
|
' Apply theme
|
||||||
|
Public Sub ApplyTheme()
|
||||||
|
Dim DarkMode As Boolean = GetDarkMode()
|
||||||
|
If DarkMode Then
|
||||||
|
StartForm.BackColor = ColorTranslator.FromHtml("#FF121212")
|
||||||
|
StartForm.ToolsToolStripMenuTools.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
|
||||||
|
StartForm.KeywordTextBox.BackColor = ColorTranslator.FromHtml("#FF2E2E2E")
|
||||||
|
StartForm.KeywordTextBox.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
|
||||||
|
StartForm.SubredditTextBox.BackColor = ColorTranslator.FromHtml("#FF2E2E2E")
|
||||||
|
StartForm.SubredditTextBox.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
|
||||||
|
StartForm.LimitNumericUpDown.BackColor = ColorTranslator.FromHtml("#FF2E2E2E")
|
||||||
|
StartForm.LimitNumericUpDown.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
|
||||||
|
StartForm.LimitNumericUpDown.BackColor = ColorTranslator.FromHtml("#FF2E2E2E")
|
||||||
|
StartForm.LimitNumericUpDown.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
|
||||||
|
StartForm.ListingComboBox.BackColor = ColorTranslator.FromHtml("#FF2E2E2E")
|
||||||
|
StartForm.ListingComboBox.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
|
||||||
|
StartForm.TimeframeComboBox.BackColor = ColorTranslator.FromHtml("#FF2E2E2E")
|
||||||
|
StartForm.TimeframeComboBox.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
|
||||||
|
StartForm.Label1.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
|
||||||
|
StartForm.Label2.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
|
||||||
|
StartForm.Label3.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
|
||||||
|
StartForm.Label4.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
|
||||||
|
StartForm.Label5.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
|
||||||
|
Else
|
||||||
|
StartForm.BackColor = ColorTranslator.FromHtml("#FFFFFFFF")
|
||||||
|
StartForm.ToolsToolStripMenuTools.ForeColor = ColorTranslator.FromHtml("#FF121212")
|
||||||
|
StartForm.KeywordTextBox.BackColor = ColorTranslator.FromHtml("#FFFFFFFF")
|
||||||
|
StartForm.KeywordTextBox.ForeColor = ColorTranslator.FromHtml("#FF121212")
|
||||||
|
StartForm.SubredditTextBox.BackColor = ColorTranslator.FromHtml("#FFFFFFFF")
|
||||||
|
StartForm.SubredditTextBox.ForeColor = ColorTranslator.FromHtml("#FF121212")
|
||||||
|
StartForm.LimitNumericUpDown.BackColor = ColorTranslator.FromHtml("#FFFFFFFF")
|
||||||
|
StartForm.LimitNumericUpDown.ForeColor = ColorTranslator.FromHtml("#FF121212")
|
||||||
|
StartForm.LimitNumericUpDown.BackColor = ColorTranslator.FromHtml("#FFFFFFFF")
|
||||||
|
StartForm.LimitNumericUpDown.ForeColor = ColorTranslator.FromHtml("#FF121212")
|
||||||
|
StartForm.ListingComboBox.BackColor = ColorTranslator.FromHtml("#FFFFFFFF")
|
||||||
|
StartForm.ListingComboBox.ForeColor = ColorTranslator.FromHtml("#FF121212")
|
||||||
|
StartForm.TimeframeComboBox.BackColor = ColorTranslator.FromHtml("#FFFFFFFF")
|
||||||
|
StartForm.TimeframeComboBox.ForeColor = ColorTranslator.FromHtml("#FF121212")
|
||||||
|
StartForm.Label1.ForeColor = ColorTranslator.FromHtml("#FF121212")
|
||||||
|
StartForm.Label2.ForeColor = ColorTranslator.FromHtml("#FF121212")
|
||||||
|
StartForm.Label3.ForeColor = ColorTranslator.FromHtml("#FF121212")
|
||||||
|
StartForm.Label4.ForeColor = ColorTranslator.FromHtml("#FF121212")
|
||||||
|
StartForm.Label5.ForeColor = ColorTranslator.FromHtml("#FF121212")
|
||||||
|
End If
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
' Get dark mode state from settings.json's key 'DarkMode'
|
||||||
|
Private Function GetDarkMode() As Boolean
|
||||||
|
If File.Exists(settingsFilePath) Then
|
||||||
|
Dim json As String = File.ReadAllText(settingsFilePath)
|
||||||
|
Dim settings As JObject = JObject.Parse(json)
|
||||||
|
Return settings("DarkMode").ToObject(Of Boolean)()
|
||||||
|
Else
|
||||||
|
Return False
|
||||||
|
End If
|
||||||
|
End Function
|
||||||
|
|
||||||
|
End Class
|
||||||
@@ -35,17 +35,19 @@ Partial Class StartForm
|
|||||||
Label4 = New Label()
|
Label4 = New Label()
|
||||||
Label5 = New Label()
|
Label5 = New Label()
|
||||||
ContextMenuStrip1 = New ContextMenuStrip(components)
|
ContextMenuStrip1 = New ContextMenuStrip(components)
|
||||||
SaveResultsJSONToolStripMenuItem = New ToolStripMenuItem()
|
SaveResultsStripMenuItem = New ToolStripMenuItem()
|
||||||
JSONToolStripMenuItem = New ToolStripMenuItem()
|
JSONToolStripMenuItem = New ToolStripMenuItem()
|
||||||
CSVToolStripMenuItem = New ToolStripMenuItem()
|
CSVToolStripMenuItem = New ToolStripMenuItem()
|
||||||
|
DarkModeToolStripMenuItem = New ToolStripMenuItem()
|
||||||
FileMenuStrip = New MenuStrip()
|
FileMenuStrip = New MenuStrip()
|
||||||
ToolsToolStripMenuTools = New ToolStripMenuItem()
|
ToolsToolStripMenuTools = New ToolStripMenuItem()
|
||||||
AboutToolStripMenuItem = New ToolStripMenuItem()
|
AboutToolStripMenuItem = New ToolStripMenuItem()
|
||||||
DeveloperToolStripMenuItem = New ToolStripMenuItem()
|
DeveloperToolStripMenuItem = New ToolStripMenuItem()
|
||||||
ChekUpdatesToolStripMenuItem = New ToolStripMenuItem()
|
CheckUpdatesToolStripMenuItem = New ToolStripMenuItem()
|
||||||
ToolStripSeparator2 = New ToolStripSeparator()
|
ToolStripSeparator2 = New ToolStripSeparator()
|
||||||
QuitToolStripMenuItem = New ToolStripMenuItem()
|
QuitToolStripMenuItem = New ToolStripMenuItem()
|
||||||
LimitNumericUpDown = New NumericUpDown()
|
LimitNumericUpDown = New NumericUpDown()
|
||||||
|
ToolTip1 = New ToolTip(components)
|
||||||
ContextMenuStrip1.SuspendLayout()
|
ContextMenuStrip1.SuspendLayout()
|
||||||
FileMenuStrip.SuspendLayout()
|
FileMenuStrip.SuspendLayout()
|
||||||
CType(LimitNumericUpDown, ComponentModel.ISupportInitialize).BeginInit()
|
CType(LimitNumericUpDown, ComponentModel.ISupportInitialize).BeginInit()
|
||||||
@@ -86,7 +88,8 @@ Partial Class StartForm
|
|||||||
TimeframeComboBox.Name = "TimeframeComboBox"
|
TimeframeComboBox.Name = "TimeframeComboBox"
|
||||||
TimeframeComboBox.Size = New Size(100, 23)
|
TimeframeComboBox.Size = New Size(100, 23)
|
||||||
TimeframeComboBox.TabIndex = 8
|
TimeframeComboBox.TabIndex = 8
|
||||||
TimeframeComboBox.Text = "All"'
|
TimeframeComboBox.Text = "All"
|
||||||
|
'
|
||||||
' ListingComboBox
|
' ListingComboBox
|
||||||
'
|
'
|
||||||
ListingComboBox.FormattingEnabled = True
|
ListingComboBox.FormattingEnabled = True
|
||||||
@@ -95,7 +98,8 @@ Partial Class StartForm
|
|||||||
ListingComboBox.Name = "ListingComboBox"
|
ListingComboBox.Name = "ListingComboBox"
|
||||||
ListingComboBox.Size = New Size(100, 23)
|
ListingComboBox.Size = New Size(100, 23)
|
||||||
ListingComboBox.TabIndex = 9
|
ListingComboBox.TabIndex = 9
|
||||||
ListingComboBox.Text = "Top"'
|
ListingComboBox.Text = "Top"
|
||||||
|
'
|
||||||
' Label1
|
' Label1
|
||||||
'
|
'
|
||||||
Label1.AutoEllipsis = True
|
Label1.AutoEllipsis = True
|
||||||
@@ -105,7 +109,8 @@ Partial Class StartForm
|
|||||||
Label1.Name = "Label1"
|
Label1.Name = "Label1"
|
||||||
Label1.Size = New Size(56, 23)
|
Label1.Size = New Size(56, 23)
|
||||||
Label1.TabIndex = 10
|
Label1.TabIndex = 10
|
||||||
Label1.Text = "Keyword"'
|
Label1.Text = "Keyword"
|
||||||
|
'
|
||||||
' Label2
|
' Label2
|
||||||
'
|
'
|
||||||
Label2.AutoEllipsis = True
|
Label2.AutoEllipsis = True
|
||||||
@@ -115,7 +120,8 @@ Partial Class StartForm
|
|||||||
Label2.Name = "Label2"
|
Label2.Name = "Label2"
|
||||||
Label2.Size = New Size(63, 23)
|
Label2.Size = New Size(63, 23)
|
||||||
Label2.TabIndex = 11
|
Label2.TabIndex = 11
|
||||||
Label2.Text = "Subreddit"'
|
Label2.Text = "Subreddit"
|
||||||
|
'
|
||||||
' Label3
|
' Label3
|
||||||
'
|
'
|
||||||
Label3.AutoEllipsis = True
|
Label3.AutoEllipsis = True
|
||||||
@@ -125,7 +131,8 @@ Partial Class StartForm
|
|||||||
Label3.Name = "Label3"
|
Label3.Name = "Label3"
|
||||||
Label3.Size = New Size(56, 23)
|
Label3.Size = New Size(56, 23)
|
||||||
Label3.TabIndex = 12
|
Label3.TabIndex = 12
|
||||||
Label3.Text = "Limit"'
|
Label3.Text = "Limit"
|
||||||
|
'
|
||||||
' Label4
|
' Label4
|
||||||
'
|
'
|
||||||
Label4.AutoEllipsis = True
|
Label4.AutoEllipsis = True
|
||||||
@@ -135,7 +142,8 @@ Partial Class StartForm
|
|||||||
Label4.Name = "Label4"
|
Label4.Name = "Label4"
|
||||||
Label4.Size = New Size(56, 23)
|
Label4.Size = New Size(56, 23)
|
||||||
Label4.TabIndex = 13
|
Label4.TabIndex = 13
|
||||||
Label4.Text = "Listing"'
|
Label4.Text = "Listing"
|
||||||
|
'
|
||||||
' Label5
|
' Label5
|
||||||
'
|
'
|
||||||
Label5.AutoEllipsis = True
|
Label5.AutoEllipsis = True
|
||||||
@@ -145,22 +153,23 @@ Partial Class StartForm
|
|||||||
Label5.Name = "Label5"
|
Label5.Name = "Label5"
|
||||||
Label5.Size = New Size(71, 23)
|
Label5.Size = New Size(71, 23)
|
||||||
Label5.TabIndex = 14
|
Label5.TabIndex = 14
|
||||||
Label5.Text = "Timeframe"'
|
Label5.Text = "Timeframe"
|
||||||
|
'
|
||||||
' ContextMenuStrip1
|
' ContextMenuStrip1
|
||||||
'
|
'
|
||||||
ContextMenuStrip1.Items.AddRange(New ToolStripItem() {SaveResultsJSONToolStripMenuItem})
|
ContextMenuStrip1.Items.AddRange(New ToolStripItem() {SaveResultsStripMenuItem, DarkModeToolStripMenuItem})
|
||||||
ContextMenuStrip1.Name = "ContextMenuStrip1"
|
ContextMenuStrip1.Name = "ContextMenuStrip1"
|
||||||
ContextMenuStrip1.Size = New Size(144, 26)
|
ContextMenuStrip1.Size = New Size(144, 48)
|
||||||
'
|
'
|
||||||
' SaveResultsJSONToolStripMenuItem
|
' SaveResultsStripMenuItem
|
||||||
|
'
|
||||||
|
SaveResultsStripMenuItem.AutoToolTip = True
|
||||||
|
SaveResultsStripMenuItem.DropDownItems.AddRange(New ToolStripItem() {JSONToolStripMenuItem, CSVToolStripMenuItem})
|
||||||
|
SaveResultsStripMenuItem.Image = CType(resources.GetObject("SaveResultsStripMenuItem.Image"), Image)
|
||||||
|
SaveResultsStripMenuItem.Name = "SaveResultsStripMenuItem"
|
||||||
|
SaveResultsStripMenuItem.Size = New Size(143, 22)
|
||||||
|
SaveResultsStripMenuItem.Text = "Save posts to"
|
||||||
'
|
'
|
||||||
SaveResultsJSONToolStripMenuItem.AutoToolTip = True
|
|
||||||
SaveResultsJSONToolStripMenuItem.DropDownItems.AddRange(New ToolStripItem() {JSONToolStripMenuItem, CSVToolStripMenuItem})
|
|
||||||
SaveResultsJSONToolStripMenuItem.Image = CType(resources.GetObject("SaveResultsJSONToolStripMenuItem.Image"), Image)
|
|
||||||
SaveResultsJSONToolStripMenuItem.Name = "SaveResultsJSONToolStripMenuItem"
|
|
||||||
SaveResultsJSONToolStripMenuItem.Size = New Size(143, 22)
|
|
||||||
SaveResultsJSONToolStripMenuItem.Text = "Save posts to"
|
|
||||||
SaveResultsJSONToolStripMenuItem.ToolTipText = "Save results to a JSON file"'
|
|
||||||
' JSONToolStripMenuItem
|
' JSONToolStripMenuItem
|
||||||
'
|
'
|
||||||
JSONToolStripMenuItem.AutoToolTip = True
|
JSONToolStripMenuItem.AutoToolTip = True
|
||||||
@@ -168,7 +177,8 @@ Partial Class StartForm
|
|||||||
JSONToolStripMenuItem.Image = CType(resources.GetObject("JSONToolStripMenuItem.Image"), Image)
|
JSONToolStripMenuItem.Image = CType(resources.GetObject("JSONToolStripMenuItem.Image"), Image)
|
||||||
JSONToolStripMenuItem.Name = "JSONToolStripMenuItem"
|
JSONToolStripMenuItem.Name = "JSONToolStripMenuItem"
|
||||||
JSONToolStripMenuItem.Size = New Size(185, 22)
|
JSONToolStripMenuItem.Size = New Size(185, 22)
|
||||||
JSONToolStripMenuItem.Text = "JSON"'
|
JSONToolStripMenuItem.Text = "JSON"
|
||||||
|
'
|
||||||
' CSVToolStripMenuItem
|
' CSVToolStripMenuItem
|
||||||
'
|
'
|
||||||
CSVToolStripMenuItem.AutoToolTip = True
|
CSVToolStripMenuItem.AutoToolTip = True
|
||||||
@@ -176,7 +186,17 @@ Partial Class StartForm
|
|||||||
CSVToolStripMenuItem.Image = CType(resources.GetObject("CSVToolStripMenuItem.Image"), Image)
|
CSVToolStripMenuItem.Image = CType(resources.GetObject("CSVToolStripMenuItem.Image"), Image)
|
||||||
CSVToolStripMenuItem.Name = "CSVToolStripMenuItem"
|
CSVToolStripMenuItem.Name = "CSVToolStripMenuItem"
|
||||||
CSVToolStripMenuItem.Size = New Size(185, 22)
|
CSVToolStripMenuItem.Size = New Size(185, 22)
|
||||||
CSVToolStripMenuItem.Text = "CSV (coming soon...)"'
|
CSVToolStripMenuItem.Text = "CSV (coming soon...)"
|
||||||
|
'
|
||||||
|
' DarkModeToolStripMenuItem
|
||||||
|
'
|
||||||
|
DarkModeToolStripMenuItem.AutoToolTip = True
|
||||||
|
DarkModeToolStripMenuItem.CheckOnClick = True
|
||||||
|
DarkModeToolStripMenuItem.Image = CType(resources.GetObject("DarkModeToolStripMenuItem.Image"), Image)
|
||||||
|
DarkModeToolStripMenuItem.Name = "DarkModeToolStripMenuItem"
|
||||||
|
DarkModeToolStripMenuItem.Size = New Size(143, 22)
|
||||||
|
DarkModeToolStripMenuItem.Text = "Dark mode"
|
||||||
|
'
|
||||||
' FileMenuStrip
|
' FileMenuStrip
|
||||||
'
|
'
|
||||||
FileMenuStrip.BackColor = Color.Transparent
|
FileMenuStrip.BackColor = Color.Transparent
|
||||||
@@ -185,58 +205,63 @@ Partial Class StartForm
|
|||||||
FileMenuStrip.Name = "FileMenuStrip"
|
FileMenuStrip.Name = "FileMenuStrip"
|
||||||
FileMenuStrip.Size = New Size(355, 24)
|
FileMenuStrip.Size = New Size(355, 24)
|
||||||
FileMenuStrip.TabIndex = 0
|
FileMenuStrip.TabIndex = 0
|
||||||
FileMenuStrip.Text = "MenuStrip1"'
|
FileMenuStrip.Text = "MenuStrip1"
|
||||||
|
'
|
||||||
' ToolsToolStripMenuTools
|
' ToolsToolStripMenuTools
|
||||||
'
|
'
|
||||||
ToolsToolStripMenuTools.DropDownItems.AddRange(New ToolStripItem() {AboutToolStripMenuItem, DeveloperToolStripMenuItem, ChekUpdatesToolStripMenuItem, ToolStripSeparator2, QuitToolStripMenuItem})
|
ToolsToolStripMenuTools.DropDownItems.AddRange(New ToolStripItem() {AboutToolStripMenuItem, DeveloperToolStripMenuItem, CheckUpdatesToolStripMenuItem, ToolStripSeparator2, QuitToolStripMenuItem})
|
||||||
ToolsToolStripMenuTools.Font = New Font("Segoe UI", 9F, FontStyle.Regular, GraphicsUnit.Point)
|
ToolsToolStripMenuTools.Font = New Font("Segoe UI", 9F, FontStyle.Regular, GraphicsUnit.Point)
|
||||||
ToolsToolStripMenuTools.ForeColor = Color.White
|
ToolsToolStripMenuTools.ForeColor = Color.White
|
||||||
ToolsToolStripMenuTools.Image = CType(resources.GetObject("ToolsToolStripMenuTools.Image"), Image)
|
ToolsToolStripMenuTools.Image = CType(resources.GetObject("ToolsToolStripMenuTools.Image"), Image)
|
||||||
ToolsToolStripMenuTools.Name = "ToolsToolStripMenuTools"
|
ToolsToolStripMenuTools.Name = "ToolsToolStripMenuTools"
|
||||||
ToolsToolStripMenuTools.Size = New Size(53, 20)
|
ToolsToolStripMenuTools.Size = New Size(28, 20)
|
||||||
ToolsToolStripMenuTools.Text = "File"'
|
'
|
||||||
' AboutToolStripMenuItem
|
' AboutToolStripMenuItem
|
||||||
'
|
'
|
||||||
AboutToolStripMenuItem.AutoToolTip = True
|
AboutToolStripMenuItem.AutoToolTip = True
|
||||||
AboutToolStripMenuItem.Image = CType(resources.GetObject("AboutToolStripMenuItem.Image"), Image)
|
AboutToolStripMenuItem.Image = CType(resources.GetObject("AboutToolStripMenuItem.Image"), Image)
|
||||||
AboutToolStripMenuItem.Name = "AboutToolStripMenuItem"
|
AboutToolStripMenuItem.Name = "AboutToolStripMenuItem"
|
||||||
AboutToolStripMenuItem.Size = New Size(152, 22)
|
AboutToolStripMenuItem.Size = New Size(180, 22)
|
||||||
AboutToolStripMenuItem.Text = "About"'
|
AboutToolStripMenuItem.Text = "About"
|
||||||
|
'
|
||||||
' DeveloperToolStripMenuItem
|
' DeveloperToolStripMenuItem
|
||||||
'
|
'
|
||||||
DeveloperToolStripMenuItem.AutoToolTip = True
|
DeveloperToolStripMenuItem.AutoToolTip = True
|
||||||
DeveloperToolStripMenuItem.Image = CType(resources.GetObject("DeveloperToolStripMenuItem.Image"), Image)
|
DeveloperToolStripMenuItem.Image = CType(resources.GetObject("DeveloperToolStripMenuItem.Image"), Image)
|
||||||
DeveloperToolStripMenuItem.Name = "DeveloperToolStripMenuItem"
|
DeveloperToolStripMenuItem.Name = "DeveloperToolStripMenuItem"
|
||||||
DeveloperToolStripMenuItem.Size = New Size(152, 22)
|
DeveloperToolStripMenuItem.Size = New Size(180, 22)
|
||||||
DeveloperToolStripMenuItem.Text = "Developer"'
|
DeveloperToolStripMenuItem.Text = "Developer"
|
||||||
' ChekUpdatesToolStripMenuItem
|
'
|
||||||
|
' CheckUpdatesToolStripMenuItem
|
||||||
|
'
|
||||||
|
CheckUpdatesToolStripMenuItem.AutoToolTip = True
|
||||||
|
CheckUpdatesToolStripMenuItem.Image = CType(resources.GetObject("CheckUpdatesToolStripMenuItem.Image"), Image)
|
||||||
|
CheckUpdatesToolStripMenuItem.Name = "CheckUpdatesToolStripMenuItem"
|
||||||
|
CheckUpdatesToolStripMenuItem.Size = New Size(180, 22)
|
||||||
|
CheckUpdatesToolStripMenuItem.Text = "Check updates"
|
||||||
'
|
'
|
||||||
ChekUpdatesToolStripMenuItem.AutoToolTip = True
|
|
||||||
ChekUpdatesToolStripMenuItem.Image = CType(resources.GetObject("ChekUpdatesToolStripMenuItem.Image"), Image)
|
|
||||||
ChekUpdatesToolStripMenuItem.Name = "ChekUpdatesToolStripMenuItem"
|
|
||||||
ChekUpdatesToolStripMenuItem.Size = New Size(152, 22)
|
|
||||||
ChekUpdatesToolStripMenuItem.Text = "Check updates"'
|
|
||||||
' ToolStripSeparator2
|
' ToolStripSeparator2
|
||||||
'
|
'
|
||||||
ToolStripSeparator2.Name = "ToolStripSeparator2"
|
ToolStripSeparator2.Name = "ToolStripSeparator2"
|
||||||
ToolStripSeparator2.Size = New Size(149, 6)
|
ToolStripSeparator2.Size = New Size(177, 6)
|
||||||
'
|
'
|
||||||
' QuitToolStripMenuItem
|
' QuitToolStripMenuItem
|
||||||
'
|
'
|
||||||
QuitToolStripMenuItem.AutoToolTip = True
|
QuitToolStripMenuItem.AutoToolTip = True
|
||||||
QuitToolStripMenuItem.Image = CType(resources.GetObject("QuitToolStripMenuItem.Image"), Image)
|
QuitToolStripMenuItem.Image = CType(resources.GetObject("QuitToolStripMenuItem.Image"), Image)
|
||||||
QuitToolStripMenuItem.Name = "QuitToolStripMenuItem"
|
QuitToolStripMenuItem.Name = "QuitToolStripMenuItem"
|
||||||
QuitToolStripMenuItem.Size = New Size(152, 22)
|
QuitToolStripMenuItem.Size = New Size(180, 22)
|
||||||
QuitToolStripMenuItem.Text = "Quit"'
|
QuitToolStripMenuItem.Text = "Quit"
|
||||||
|
'
|
||||||
' LimitNumericUpDown
|
' LimitNumericUpDown
|
||||||
'
|
'
|
||||||
LimitNumericUpDown.Location = New Point(89, 125)
|
LimitNumericUpDown.Location = New Point(89, 125)
|
||||||
LimitNumericUpDown.Minimum = New [Decimal](New Integer() {5, 0, 0, 0})
|
LimitNumericUpDown.Minimum = New Decimal(New Integer() {5, 0, 0, 0})
|
||||||
LimitNumericUpDown.Name = "LimitNumericUpDown"
|
LimitNumericUpDown.Name = "LimitNumericUpDown"
|
||||||
LimitNumericUpDown.ReadOnly = True
|
LimitNumericUpDown.ReadOnly = True
|
||||||
LimitNumericUpDown.Size = New Size(100, 23)
|
LimitNumericUpDown.Size = New Size(100, 23)
|
||||||
LimitNumericUpDown.TabIndex = 15
|
LimitNumericUpDown.TabIndex = 15
|
||||||
LimitNumericUpDown.Value = New [Decimal](New Integer() {5, 0, 0, 0})
|
LimitNumericUpDown.Value = New Decimal(New Integer() {10, 0, 0, 0})
|
||||||
'
|
'
|
||||||
' StartForm
|
' StartForm
|
||||||
'
|
'
|
||||||
@@ -263,7 +288,7 @@ Partial Class StartForm
|
|||||||
MaximizeBox = False
|
MaximizeBox = False
|
||||||
Name = "StartForm"
|
Name = "StartForm"
|
||||||
StartPosition = FormStartPosition.CenterScreen
|
StartPosition = FormStartPosition.CenterScreen
|
||||||
Text = "Reddit Post Scraping Tool"
|
Text = "ProgramName ProgramVersion"
|
||||||
ContextMenuStrip1.ResumeLayout(False)
|
ContextMenuStrip1.ResumeLayout(False)
|
||||||
FileMenuStrip.ResumeLayout(False)
|
FileMenuStrip.ResumeLayout(False)
|
||||||
FileMenuStrip.PerformLayout()
|
FileMenuStrip.PerformLayout()
|
||||||
@@ -289,9 +314,11 @@ Partial Class StartForm
|
|||||||
Friend WithEvents DeveloperToolStripMenuItem As ToolStripMenuItem
|
Friend WithEvents DeveloperToolStripMenuItem As ToolStripMenuItem
|
||||||
Friend WithEvents ToolStripSeparator2 As ToolStripSeparator
|
Friend WithEvents ToolStripSeparator2 As ToolStripSeparator
|
||||||
Friend WithEvents QuitToolStripMenuItem As ToolStripMenuItem
|
Friend WithEvents QuitToolStripMenuItem As ToolStripMenuItem
|
||||||
Friend WithEvents SaveResultsJSONToolStripMenuItem As ToolStripMenuItem
|
Friend WithEvents SaveResultsStripMenuItem As ToolStripMenuItem
|
||||||
Friend WithEvents ChekUpdatesToolStripMenuItem As ToolStripMenuItem
|
Friend WithEvents CheckUpdatesToolStripMenuItem As ToolStripMenuItem
|
||||||
Friend WithEvents JSONToolStripMenuItem As ToolStripMenuItem
|
Friend WithEvents JSONToolStripMenuItem As ToolStripMenuItem
|
||||||
Friend WithEvents CSVToolStripMenuItem As ToolStripMenuItem
|
Friend WithEvents CSVToolStripMenuItem As ToolStripMenuItem
|
||||||
Friend WithEvents LimitNumericUpDown As NumericUpDown
|
Friend WithEvents LimitNumericUpDown As NumericUpDown
|
||||||
|
Friend WithEvents DarkModeToolStripMenuItem As ToolStripMenuItem
|
||||||
|
Friend WithEvents ToolTip1 As ToolTip
|
||||||
End Class
|
End Class
|
||||||
@@ -1,4 +1,64 @@
|
|||||||
<root>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<root>
|
||||||
|
<!--
|
||||||
|
Microsoft ResX Schema
|
||||||
|
|
||||||
|
Version 2.0
|
||||||
|
|
||||||
|
The primary goals of this format is to allow a simple XML format
|
||||||
|
that is mostly human readable. The generation and parsing of the
|
||||||
|
various data types are done through the TypeConverter classes
|
||||||
|
associated with the data types.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
... ado.net/XML headers & schema ...
|
||||||
|
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||||
|
<resheader name="version">2.0</resheader>
|
||||||
|
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||||
|
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||||
|
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||||
|
<data name="Color1" type="System.Drawing.Color, System.Drawing"">Blue</data>
|
||||||
|
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||||
|
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||||
|
</data>
|
||||||
|
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||||
|
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||||
|
<comment>This is a comment</comment>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
There are any number of "resheader" rows that contain simple
|
||||||
|
name/value pairs.
|
||||||
|
|
||||||
|
Each data row contains a name, and value. The row also contains a
|
||||||
|
type or mimetype. Type corresponds to a .NET class that support
|
||||||
|
text/value conversion through the TypeConverter architecture.
|
||||||
|
Classes that don't support this are serialized and stored with the
|
||||||
|
mimetype set.
|
||||||
|
|
||||||
|
The mimetype is used for serialized objects, and tells the
|
||||||
|
ResXResourceReader how to depersist the object. This is currently not
|
||||||
|
extensible. For a given mimetype the value must be set accordingly:
|
||||||
|
|
||||||
|
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||||
|
that the ResXResourceWriter will generate, however the reader can
|
||||||
|
read any of the formats listed below.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.binary.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.soap.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||||
|
value : The object must be serialized into a byte array
|
||||||
|
: using a System.ComponentModel.TypeConverter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
-->
|
||||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||||
<xsd:element name="root" msdata:IsDataSet="true">
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
@@ -61,7 +121,7 @@
|
|||||||
<value>132, 17</value>
|
<value>132, 17</value>
|
||||||
</metadata>
|
</metadata>
|
||||||
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
|
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
|
||||||
<data name="SaveResultsJSONToolStripMenuItem.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
<data name="SaveResultsStripMenuItem.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||||
<value>
|
<value>
|
||||||
iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAYAAAD0eNT6AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO
|
iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAYAAAD0eNT6AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO
|
||||||
vAAADrwBlbxySQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACoYSURBVHhe7d0L
|
vAAADrwBlbxySQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACoYSURBVHhe7d0L
|
||||||
@@ -574,6 +634,268 @@
|
|||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJbvZS/7
|
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJbvZS/7
|
||||||
/4YzHMz4V1N2AAAAAElFTkSuQmCC
|
/4YzHMz4V1N2AAAAAElFTkSuQmCC
|
||||||
|
</value>
|
||||||
|
</data>
|
||||||
|
<data name="DarkModeToolStripMenuItem.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||||
|
<value>
|
||||||
|
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
|
||||||
</value>
|
</value>
|
||||||
</data>
|
</data>
|
||||||
<metadata name="FileMenuStrip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
<metadata name="FileMenuStrip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||||
@@ -1425,7 +1747,7 @@
|
|||||||
RK5CYII=
|
RK5CYII=
|
||||||
</value>
|
</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="ChekUpdatesToolStripMenuItem.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
<data name="CheckUpdatesToolStripMenuItem.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||||
<value>
|
<value>
|
||||||
iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAYAAAD0eNT6AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAL
|
iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAYAAAD0eNT6AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAL
|
||||||
FQAACxUBgJnYgwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAGmYSURBVHhe7Z0J
|
FQAACxUBgJnYgwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAGmYSURBVHhe7Z0J
|
||||||
@@ -1893,6 +2215,9 @@
|
|||||||
AY7pF0Fg9EWkAAAAAElFTkSuQmCC
|
AY7pF0Fg9EWkAAAAAElFTkSuQmCC
|
||||||
</value>
|
</value>
|
||||||
</data>
|
</data>
|
||||||
|
<metadata name="ToolTip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||||
|
<value>289, 17</value>
|
||||||
|
</metadata>
|
||||||
<data name="$this.Icon" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
<data name="$this.Icon" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||||
<value>
|
<value>
|
||||||
AAABAAEAAAAAAAEAIACvPgAAFgAAAIlQTkcNChoKAAAADUlIRFIAAAEAAAABAAgGAAAAXHKoZgAAPnZJ
|
AAABAAEAAAAAAAEAIACvPgAAFgAAAIlQTkcNChoKAAAADUlIRFIAAAEAAAABAAgGAAAAXHKoZgAAPnZJ
|
||||||
105
RPST GUI/RPST/StartForm.vb
Normal file
105
RPST GUI/RPST/StartForm.vb
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
Imports System.IO
|
||||||
|
Imports Newtonsoft.Json
|
||||||
|
Imports Newtonsoft.Json.Linq
|
||||||
|
|
||||||
|
Public Class StartForm
|
||||||
|
ReadOnly settings As New SettingsManager()
|
||||||
|
ReadOnly ApiHandler As New ApiHandler()
|
||||||
|
|
||||||
|
''' <summary>
|
||||||
|
''' Handles the click event of the ScrapeButton.
|
||||||
|
''' Collects inputs, fetches Reddit posts based on the inputs,
|
||||||
|
''' and updates the DataGridView with the fetched posts.
|
||||||
|
''' </summary>
|
||||||
|
''' <param name="sender">The sender of the event.</param>
|
||||||
|
''' <param name="e">The EventArgs instance containing the event data.</param>
|
||||||
|
Private Sub ScrapeButton_Click(sender As Object, e As EventArgs) Handles ScrapeButton.Click
|
||||||
|
Utilities.ProcessRedditPosts(JSONToolStripMenuItem)
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
|
||||||
|
''' <summary>
|
||||||
|
''' Event handler for the form load event.
|
||||||
|
''' It loads settings, toggles dark mode if necessary, checks for directories, logs first time launch, and sets the form title.
|
||||||
|
''' </summary>
|
||||||
|
''' <param name="sender">The source of the event.</param>
|
||||||
|
''' <param name="e">An EventArgs that contains the event data.</param>
|
||||||
|
Private Sub StartForm_Load(sender As Object, e As EventArgs) Handles MyBase.Load
|
||||||
|
settings.LoadSettings()
|
||||||
|
settings.ToggleDarkMode(settings.DarkMode)
|
||||||
|
Utilities.PathFinder()
|
||||||
|
Utilities.LogFirstTimeLaunch()
|
||||||
|
Me.Text = My.Application.Info.AssemblyName
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
|
||||||
|
''' <summary>
|
||||||
|
''' Event handler for the 'About' menu item click.
|
||||||
|
''' It shows the 'About' dialog box.
|
||||||
|
''' </summary>
|
||||||
|
''' <param name="sender">The source of the event.</param>
|
||||||
|
''' <param name="e">An EventArgs that contains the event data.</param>
|
||||||
|
Private Sub AboutToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles AboutToolStripMenuItem.Click
|
||||||
|
AboutBox.ShowDialog()
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
''' <summary>
|
||||||
|
''' Event handler for the 'Quit' menu item click.
|
||||||
|
''' It asks the user for confirmation and closes the program if the user agrees.
|
||||||
|
''' </summary>
|
||||||
|
''' <param name="sender">The source of the event.</param>
|
||||||
|
''' <param name="e">An EventArgs that contains the event data.</param>
|
||||||
|
Private Sub QuitToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles QuitToolStripMenuItem.Click
|
||||||
|
Dim result As DialogResult = MessageBox.Show("This will close the program, continue?", "Quit", MessageBoxButtons.YesNo, MessageBoxIcon.Question)
|
||||||
|
If result = DialogResult.Yes Then
|
||||||
|
Me.Close()
|
||||||
|
End If
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
|
||||||
|
''' <summary>
|
||||||
|
''' Event handler for the 'Developer' menu item click.
|
||||||
|
''' It shows the 'Developer' dialog box.
|
||||||
|
''' </summary>
|
||||||
|
''' <param name="sender">The source of the event.</param>
|
||||||
|
''' <param name="e">An EventArgs that contains the event data.</param>
|
||||||
|
Private Sub DeveloperToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles DeveloperToolStripMenuItem.Click
|
||||||
|
DeveloperForm.ShowDialog()
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
|
||||||
|
''' <summary>
|
||||||
|
''' Event handler for the 'Check Updates' menu item click.
|
||||||
|
''' It checks for application updates and provides update information if a newer version is available.
|
||||||
|
''' </summary>
|
||||||
|
''' <param name="sender">The source of the event.</param>
|
||||||
|
''' <param name="e">An EventArgs that contains the event data.</param>
|
||||||
|
Private Sub CheckUpdatesToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles CheckUpdatesToolStripMenuItem.Click
|
||||||
|
|
||||||
|
Dim data As JObject = ApiHandler.CheckUpdates()
|
||||||
|
If data("tag_name").ToString = $"{My.Application.Info.Version}" Then
|
||||||
|
MessageBox.Show($"You're running the current version v{My.Application.Info.Version} of {My.Application.Info.ProductName}. Check again soon! :)", "Information", MessageBoxButtons.OK, MessageBoxIcon.Information)
|
||||||
|
Else
|
||||||
|
Dim confirm As DialogResult = MessageBox.Show($"A new version v{data("tag_name")} of {My.Application.Info.ProductName} is available, would you like to get it?
|
||||||
|
|
||||||
|
What's new in v{data("tag_name")}?
|
||||||
|
{data("body")}
|
||||||
|
", "Update", MessageBoxButtons.YesNo, MessageBoxIcon.Question)
|
||||||
|
If confirm = DialogResult.Yes Then
|
||||||
|
Shell($"cmd /c start https://github.com/bellingcat/reddit-post-scraping-tool/releases/tag/{data("tag_name")}")
|
||||||
|
End If
|
||||||
|
End If
|
||||||
|
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
|
||||||
|
''' <summary>
|
||||||
|
''' Event handler for the 'Dark Mode' checkbox change event.
|
||||||
|
''' It toggles the dark mode of the application based on the checkbox status.
|
||||||
|
''' </summary>
|
||||||
|
''' <param name="sender">The source of the event.</param>
|
||||||
|
''' <param name="e">An EventArgs that contains the event data.</param>
|
||||||
|
Private Sub DarkModeToolStripMenuItem_CheckedChanged(sender As Object, e As EventArgs) Handles DarkModeToolStripMenuItem.CheckedChanged
|
||||||
|
settings.ToggleDarkMode(DarkModeToolStripMenuItem.Checked)
|
||||||
|
End Sub
|
||||||
|
End Class
|
||||||
180
RPST GUI/RPST/Utilities.vb
Normal file
180
RPST GUI/RPST/Utilities.vb
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
Imports System.IO
|
||||||
|
Imports Newtonsoft.Json
|
||||||
|
Imports Newtonsoft.Json.Linq
|
||||||
|
|
||||||
|
Public Class Utilities
|
||||||
|
''' <summary>
|
||||||
|
''' Collects user inputs, fetches Reddit posts based on the inputs, checks if posts contain the keyword, and saves posts to a JSON file if necessary.
|
||||||
|
''' </summary>
|
||||||
|
''' <param name="JSONToolStripMenuItem">Indicates whether to save the posts to a JSON file.</param>
|
||||||
|
''' <remarks>
|
||||||
|
''' This function initializes the DataGridView, iterates over each post, adds the posts containing the keyword to the DataGridView and updates the UI.
|
||||||
|
''' It also shows a message if the keyword was not found in any of the posts or if the inputs are empty.
|
||||||
|
''' </remarks>
|
||||||
|
Public Shared Sub ProcessRedditPosts(JSONToolStripMenuItem As ToolStripMenuItem)
|
||||||
|
' Collect inputs from the user
|
||||||
|
Dim inputs = CollectInputs()
|
||||||
|
|
||||||
|
If inputs.HasValue Then
|
||||||
|
' Initialize the DataGridView
|
||||||
|
DataGridViewHandler.AddColumn(PostsForm.DataGridViewPosts)
|
||||||
|
|
||||||
|
' Fetch Reddit posts based on the inputs
|
||||||
|
Dim processor As New PostsProcessor()
|
||||||
|
Dim posts As JObject = processor.FetchPosts(inputs.Value.Subreddit, inputs.Value.Listing, inputs.Value.Limit, inputs.Value.Timeframe)
|
||||||
|
Dim totalPosts As Integer = 0
|
||||||
|
Dim keywordFound As Boolean = False
|
||||||
|
|
||||||
|
' Iterate over each post
|
||||||
|
For Each post In posts("data")("children")
|
||||||
|
totalPosts += 1
|
||||||
|
' Check if the post contains the keyword
|
||||||
|
If PostsProcessor.PostContainsKeyword(post, inputs.Value.Keyword.ToLower(System.Globalization.CultureInfo.InvariantCulture)) Then
|
||||||
|
' Add the post to the DataGridView
|
||||||
|
DataGridViewHandler.AddRow(PostsForm.DataGridViewPosts, post, totalPosts)
|
||||||
|
PostsForm.Show()
|
||||||
|
keywordFound = True
|
||||||
|
End If
|
||||||
|
Next
|
||||||
|
|
||||||
|
' Check if the keyword was found in any posts
|
||||||
|
If Not keywordFound Then
|
||||||
|
MessageBox.Show($"Keyword `{inputs.Value.Keyword}` was not found in any of the " + posts("data")("children").Count.ToString(System.Globalization.CultureInfo.InvariantCulture) _
|
||||||
|
+ $" {inputs.Value.Listing} posts from r/{inputs.Value.Subreddit}", "Not Found", MessageBoxButtons.OK, MessageBoxIcon.Warning)
|
||||||
|
End If
|
||||||
|
|
||||||
|
If JSONToolStripMenuItem.Checked Then
|
||||||
|
' Save posts to a JSON file if the JSONToolStripMenuItem is checked
|
||||||
|
Utilities.SavePostsToJson(posts("data"))
|
||||||
|
End If
|
||||||
|
Else
|
||||||
|
MessageBox.Show("Inputs cannot be empty. Please enter a keyword and a subreddit.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
|
||||||
|
End If
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
|
||||||
|
''' <summary>
|
||||||
|
''' Checks for the existence of the 'logs' directory under the 'RPST' directory within the user's AppData\Roaming folder.
|
||||||
|
''' If the directory does not exist, it creates one.
|
||||||
|
''' </summary>
|
||||||
|
''' <remarks>
|
||||||
|
''' The directory path is 'C:\Users\<username>\AppData\Roaming\RPST\logs'.
|
||||||
|
''' If the 'RPST' or 'logs' directories do not exist, the function will create them.
|
||||||
|
''' If the directories already exist, the function will not perform any actions.
|
||||||
|
''' </remarks>
|
||||||
|
Public Shared Sub PathFinder()
|
||||||
|
Dim directoryPath As String = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RPST", "logs")
|
||||||
|
|
||||||
|
If Not Directory.Exists(directoryPath) Then
|
||||||
|
Directory.CreateDirectory(directoryPath)
|
||||||
|
End If
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
|
||||||
|
''' <summary>
|
||||||
|
''' Collects and validates user inputs from StartForm and returns them as a Tuple.
|
||||||
|
''' </summary>
|
||||||
|
''' <returns>
|
||||||
|
''' Tuple containing:
|
||||||
|
''' Keyword (String) - Keyword entered by user in the StartForm.
|
||||||
|
''' Subreddit (String) - Subreddit entered by user in the StartForm.
|
||||||
|
''' Listing (String) - Listing chosen by user in the StartForm, defaults to 'top' if none is selected.
|
||||||
|
''' Limit (Integer) - Limit entered by user in the StartForm, defaults to 10 if the entered value is over 100.
|
||||||
|
''' Timeframe (String) - Timeframe chosen by user in the StartForm, defaults to 'all' if none is selected.
|
||||||
|
''' </returns>
|
||||||
|
''' <remarks>
|
||||||
|
''' If keyword or subreddit are empty, Displays a warning and returns nothing.
|
||||||
|
''' </remarks>
|
||||||
|
Public Shared Function CollectInputs() As (Keyword As String, Subreddit As String, Listing As String, Limit As Integer, Timeframe As String)?
|
||||||
|
Dim keyword As String = StartForm.KeywordTextBox.Text.Trim()
|
||||||
|
Dim subreddit As String = StartForm.SubredditTextBox.Text.Trim()
|
||||||
|
' Convert the Keyword and Subreddit to lowercase using InvariantCulture
|
||||||
|
Dim listing As String = If(String.IsNullOrEmpty(StartForm.ListingComboBox.Text), "top", StartForm.ListingComboBox.Text.ToLower(System.Globalization.CultureInfo.InvariantCulture).Trim())
|
||||||
|
Dim limit As Integer = StartForm.LimitNumericUpDown.Value
|
||||||
|
Dim timeframe As String = If(String.IsNullOrEmpty(StartForm.TimeframeComboBox.Text), "all", StartForm.TimeframeComboBox.Text.ToLower(System.Globalization.CultureInfo.InvariantCulture).Trim())
|
||||||
|
|
||||||
|
' Validate inputs
|
||||||
|
If String.IsNullOrEmpty(keyword) Then
|
||||||
|
MessageBox.Show("Keyword should not be empty", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning)
|
||||||
|
Return Nothing
|
||||||
|
End If
|
||||||
|
If String.IsNullOrEmpty(subreddit) Then
|
||||||
|
MessageBox.Show("Subreddit should not be empty", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning)
|
||||||
|
Return Nothing
|
||||||
|
End If
|
||||||
|
If limit > 100 Then
|
||||||
|
MessageBox.Show("Limit should not be over 100. Defaulting to 10", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning)
|
||||||
|
limit = 10
|
||||||
|
End If
|
||||||
|
|
||||||
|
Return (keyword, subreddit, listing, limit, timeframe)
|
||||||
|
End Function
|
||||||
|
|
||||||
|
|
||||||
|
''' <summary>
|
||||||
|
''' Saves the gives posts' data to a JSON file.
|
||||||
|
''' </summary>
|
||||||
|
''' <param name="Posts">The object containing posts to be saved.</param>
|
||||||
|
''' <remarks>
|
||||||
|
''' This function allows the user to select a location to save the posts.
|
||||||
|
''' If the user confirms the save location, the posts will be serialized
|
||||||
|
''' to JSON with an indented format and written to the chosen file.
|
||||||
|
''' A success message will be displayed to the user upon successful save.
|
||||||
|
''' </remarks>
|
||||||
|
Public Shared Sub SavePostsToJson(Posts As Object)
|
||||||
|
Dim saveFileDialog As New SaveFileDialog With {
|
||||||
|
.Filter = "JSON files (*.json)|*.json",
|
||||||
|
.Title = "Save posts to JSON"
|
||||||
|
}
|
||||||
|
|
||||||
|
If saveFileDialog.ShowDialog() = DialogResult.OK Then
|
||||||
|
Dim fileName As String = saveFileDialog.FileName
|
||||||
|
Dim serializerSettings As New JsonSerializerSettings With {
|
||||||
|
.Formatting = Formatting.Indented
|
||||||
|
}
|
||||||
|
Dim json As String = JsonConvert.SerializeObject(Posts, serializerSettings)
|
||||||
|
|
||||||
|
File.WriteAllText(fileName, json)
|
||||||
|
|
||||||
|
MessageBox.Show($"Posts saved to {fileName}", "Success", MessageBoxButtons.OK, MessageBoxIcon.Information)
|
||||||
|
End If
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
|
||||||
|
''' <summary>
|
||||||
|
''' Shows the license notice in a messagebox.
|
||||||
|
''' </summary>
|
||||||
|
''' <remarks>
|
||||||
|
''' The license text is retrieved from the AboutBox.LicenseText property.
|
||||||
|
''' The messagebox is displayed with the title "License" and an information icon.
|
||||||
|
''' </remarks>
|
||||||
|
Public Shared Sub LicenseNotice()
|
||||||
|
MessageBox.Show(AboutBox.LicenseText, "License", MessageBoxButtons.OK, MessageBoxIcon.Information)
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
|
||||||
|
''' <summary>
|
||||||
|
''' Checks if the "first-launch.log" file exists in the directory: C:\Users\<username>\AppData\Roaming\RedditPostScrapingTool\logs.
|
||||||
|
''' If the file doesn't exist, it creates one. This file is used to determine whether the program has been run before.
|
||||||
|
''' If the program is being run for the first time, a license notice will be displayed.
|
||||||
|
''' </summary>
|
||||||
|
Public Shared Sub LogFirstTimeLaunch()
|
||||||
|
Dim filePath As String = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RPST", "logs", "first-launch.log")
|
||||||
|
Dim textToWrite As String = $"
|
||||||
|
{My.Application.Info.AssemblyName}
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
|
||||||
|
User: {Environment.UserName}
|
||||||
|
Host: {Environment.MachineName}
|
||||||
|
OS: {Environment.OSVersion}
|
||||||
|
x64: {Environment.Is64BitOperatingSystem}
|
||||||
|
First launched on: {DateTime.Now}"
|
||||||
|
If Not File.Exists(filePath) Then
|
||||||
|
LicenseNotice()
|
||||||
|
File.WriteAllText(filePath, textToWrite)
|
||||||
|
Else
|
||||||
|
' DO NOTHING
|
||||||
|
End If
|
||||||
|
End Sub
|
||||||
|
End Class
|
||||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
@@ -1,14 +0,0 @@
|
|||||||
Public Class AboutForm
|
|
||||||
Private Sub AboutForm_Load(sender As Object, e As EventArgs) Handles MyBase.Load
|
|
||||||
DescriptionLabel.Text = "Given a subreddit name and a keyword,
|
|
||||||
Reddit Post Scraping Tool returns all top posts
|
|
||||||
(by default) that contain the specified keyword."
|
|
||||||
VersionLabel.Text = $"v{My.Application.Info.Version}"
|
|
||||||
LicenseRichTextBox.Text = StartForm.LicenseText
|
|
||||||
End Sub
|
|
||||||
|
|
||||||
Private Sub LinkLabel1_LinkClicked(sender As Object, e As LinkLabelLinkClickedEventArgs) Handles WikiLinkLabel.LinkClicked
|
|
||||||
Shell("cmd /c start https://github.com/bellingcat/reddit-post-scraping-tool/wiki")
|
|
||||||
End Sub
|
|
||||||
|
|
||||||
End Class
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
Imports System.IO
|
|
||||||
Imports System.Net.Http
|
|
||||||
Imports Newtonsoft.Json
|
|
||||||
Imports Newtonsoft.Json.Linq
|
|
||||||
|
|
||||||
Public Class ApiHandler
|
|
||||||
Public logfile As String = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RedditPostScrapingTool", "logs", $"debug.log")
|
|
||||||
Public headers As String = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1.2 Safari/605.1.15"
|
|
||||||
Public UpdatesEndpoint As String = "https://api.github.com/repos/bellingcat/reddit-post-scraping-tool/releases/latest"
|
|
||||||
|
|
||||||
Public Function ScrapeReddit(subreddit, listing, limit, timeframe) As JObject
|
|
||||||
Dim ApiEndpoint As String = $"https://reddit.com/r/{subreddit}/{listing}.json?limit={limit}&t={timeframe}').json()"
|
|
||||||
Try
|
|
||||||
Dim httpClient As New HttpClient()
|
|
||||||
httpClient.DefaultRequestHeaders.Add("User-Agent", headers)
|
|
||||||
Dim response As HttpResponseMessage = httpClient.GetAsync(ApiEndpoint).Result
|
|
||||||
If response.IsSuccessStatusCode Then
|
|
||||||
Dim json As String = response.Content.ReadAsStringAsync().Result
|
|
||||||
Dim data As JObject = JsonConvert.DeserializeObject(Of JObject)(json)
|
|
||||||
Return data
|
|
||||||
Else
|
|
||||||
' handle the case when the response status is not successful
|
|
||||||
' return an empty JObject or throw an exception
|
|
||||||
Return New JObject()
|
|
||||||
MessageBox.Show(response.ReasonPhrase, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
|
|
||||||
End If
|
|
||||||
Catch ex As Exception
|
|
||||||
' handle the exception
|
|
||||||
' return an empty JObject or throw an exception
|
|
||||||
Return New JObject()
|
|
||||||
My.Computer.FileSystem.WriteAllText(logfile, $"{DateTime.Now}: {ex}{Environment.NewLine}", True)
|
|
||||||
MessageBox.Show($"{ex.Message}. Please see the debug log '{logfile}' for more information.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
|
|
||||||
End Try
|
|
||||||
Return New JObject()
|
|
||||||
End Function
|
|
||||||
|
|
||||||
|
|
||||||
' Gets remote version information from the repository release page
|
|
||||||
Public Function CheckUpdates() As JObject
|
|
||||||
Try
|
|
||||||
Dim httpClient As New HttpClient()
|
|
||||||
httpClient.DefaultRequestHeaders.Add("User-Agent", headers)
|
|
||||||
Dim response As HttpResponseMessage = httpClient.GetAsync(UpdatesEndpoint).Result
|
|
||||||
If response.IsSuccessStatusCode Then
|
|
||||||
Dim json As String = response.Content.ReadAsStringAsync().Result
|
|
||||||
Dim data As JObject = JsonConvert.DeserializeObject(Of JObject)(json)
|
|
||||||
Return data
|
|
||||||
Else
|
|
||||||
'Return New JObject()
|
|
||||||
MessageBox.Show(response.ReasonPhrase, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
|
|
||||||
End If
|
|
||||||
Catch ex As Exception
|
|
||||||
'Return New JObject()
|
|
||||||
My.Computer.FileSystem.WriteAllText(logfile, $"{DateTime.Now}: {ex}{Environment.NewLine}", True)
|
|
||||||
MessageBox.Show($"{ex.Message}. Please see the debug log '{logfile}' for more information.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
|
|
||||||
End Try
|
|
||||||
Return New JObject()
|
|
||||||
End Function
|
|
||||||
End Class
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()>
|
|
||||||
Partial Class DeveloperForm
|
|
||||||
Inherits System.Windows.Forms.Form
|
|
||||||
|
|
||||||
'Form overrides dispose to clean up the component list.
|
|
||||||
<System.Diagnostics.DebuggerNonUserCode()>
|
|
||||||
Protected Overrides Sub Dispose(ByVal disposing As Boolean)
|
|
||||||
Try
|
|
||||||
If disposing AndAlso components IsNot Nothing Then
|
|
||||||
components.Dispose()
|
|
||||||
End If
|
|
||||||
Finally
|
|
||||||
MyBase.Dispose(disposing)
|
|
||||||
End Try
|
|
||||||
End Sub
|
|
||||||
|
|
||||||
'Required by the Windows Form Designer
|
|
||||||
Private components As System.ComponentModel.IContainer
|
|
||||||
|
|
||||||
'NOTE: The following procedure is required by the Windows Form Designer
|
|
||||||
'It can be modified using the Windows Form Designer.
|
|
||||||
'Do not modify it using the code editor.
|
|
||||||
<System.Diagnostics.DebuggerStepThrough()>
|
|
||||||
Private Sub InitializeComponent()
|
|
||||||
Dim resources As System.ComponentModel.ComponentResourceManager = New System.ComponentModel.ComponentResourceManager(GetType(DeveloperForm))
|
|
||||||
Me.AboutMeLinkLabel = New System.Windows.Forms.LinkLabel()
|
|
||||||
Me.BuyMeACoffeeLinkLabel = New System.Windows.Forms.LinkLabel()
|
|
||||||
Me.GreetingLabel = New System.Windows.Forms.Label()
|
|
||||||
Me.SuspendLayout()
|
|
||||||
'
|
|
||||||
'AboutMeLinkLabel
|
|
||||||
'
|
|
||||||
Me.AboutMeLinkLabel.AutoSize = True
|
|
||||||
Me.AboutMeLinkLabel.BackColor = System.Drawing.Color.White
|
|
||||||
Me.AboutMeLinkLabel.Location = New System.Drawing.Point(33, 426)
|
|
||||||
Me.AboutMeLinkLabel.Name = "AboutMeLinkLabel"
|
|
||||||
Me.AboutMeLinkLabel.Size = New System.Drawing.Size(60, 15)
|
|
||||||
Me.AboutMeLinkLabel.TabIndex = 0
|
|
||||||
Me.AboutMeLinkLabel.TabStop = True
|
|
||||||
Me.AboutMeLinkLabel.Text = "About.me"
|
|
||||||
'
|
|
||||||
'BuyMeACoffeeLinkLabel
|
|
||||||
'
|
|
||||||
Me.BuyMeACoffeeLinkLabel.AutoSize = True
|
|
||||||
Me.BuyMeACoffeeLinkLabel.Location = New System.Drawing.Point(33, 451)
|
|
||||||
Me.BuyMeACoffeeLinkLabel.Name = "BuyMeACoffeeLinkLabel"
|
|
||||||
Me.BuyMeACoffeeLinkLabel.Size = New System.Drawing.Size(96, 15)
|
|
||||||
Me.BuyMeACoffeeLinkLabel.TabIndex = 1
|
|
||||||
Me.BuyMeACoffeeLinkLabel.TabStop = True
|
|
||||||
Me.BuyMeACoffeeLinkLabel.Text = "Buy Me A Coffee"
|
|
||||||
'
|
|
||||||
'GreetingLabel
|
|
||||||
'
|
|
||||||
Me.GreetingLabel.AutoSize = True
|
|
||||||
Me.GreetingLabel.Font = New System.Drawing.Font("Verdana", 27.75!, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point)
|
|
||||||
Me.GreetingLabel.Location = New System.Drawing.Point(62, 22)
|
|
||||||
Me.GreetingLabel.Name = "GreetingLabel"
|
|
||||||
Me.GreetingLabel.Size = New System.Drawing.Size(382, 45)
|
|
||||||
Me.GreetingLabel.TabIndex = 3
|
|
||||||
Me.GreetingLabel.Text = "Hello, I'm Ritchie"
|
|
||||||
'
|
|
||||||
'DeveloperForm
|
|
||||||
'
|
|
||||||
Me.AutoScaleDimensions = New System.Drawing.SizeF(7.0!, 15.0!)
|
|
||||||
Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font
|
|
||||||
Me.BackgroundImage = CType(resources.GetObject("$this.BackgroundImage"), System.Drawing.Image)
|
|
||||||
Me.ClientSize = New System.Drawing.Size(510, 510)
|
|
||||||
Me.Controls.Add(Me.BuyMeACoffeeLinkLabel)
|
|
||||||
Me.Controls.Add(Me.AboutMeLinkLabel)
|
|
||||||
Me.Controls.Add(Me.GreetingLabel)
|
|
||||||
Me.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle
|
|
||||||
Me.MaximizeBox = False
|
|
||||||
Me.Name = "DeveloperForm"
|
|
||||||
Me.ShowIcon = False
|
|
||||||
Me.ShowInTaskbar = False
|
|
||||||
Me.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent
|
|
||||||
Me.Text = "Developer"
|
|
||||||
Me.ResumeLayout(False)
|
|
||||||
Me.PerformLayout()
|
|
||||||
|
|
||||||
End Sub
|
|
||||||
|
|
||||||
Friend WithEvents AboutMeLinkLabel As LinkLabel
|
|
||||||
Friend WithEvents BuyMeACoffeeLinkLabel As LinkLabel
|
|
||||||
Friend WithEvents PictureBox1 As PictureBox
|
|
||||||
Friend WithEvents GreetingLabel As Label
|
|
||||||
End Class
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()> _
|
|
||||||
Partial Class PostsForm
|
|
||||||
Inherits System.Windows.Forms.Form
|
|
||||||
|
|
||||||
'Form overrides dispose to clean up the component list.
|
|
||||||
<System.Diagnostics.DebuggerNonUserCode()> _
|
|
||||||
Protected Overrides Sub Dispose(ByVal disposing As Boolean)
|
|
||||||
Try
|
|
||||||
If disposing AndAlso components IsNot Nothing Then
|
|
||||||
components.Dispose()
|
|
||||||
End If
|
|
||||||
Finally
|
|
||||||
MyBase.Dispose(disposing)
|
|
||||||
End Try
|
|
||||||
End Sub
|
|
||||||
|
|
||||||
'Required by the Windows Form Designer
|
|
||||||
Private components As System.ComponentModel.IContainer
|
|
||||||
|
|
||||||
'NOTE: The following procedure is required by the Windows Form Designer
|
|
||||||
'It can be modified using the Windows Form Designer.
|
|
||||||
'Do not modify it using the code editor.
|
|
||||||
<System.Diagnostics.DebuggerStepThrough()> _
|
|
||||||
Private Sub InitializeComponent()
|
|
||||||
Me.DataGridViewPosts = New System.Windows.Forms.DataGridView()
|
|
||||||
CType(Me.DataGridViewPosts, System.ComponentModel.ISupportInitialize).BeginInit()
|
|
||||||
Me.SuspendLayout()
|
|
||||||
'
|
|
||||||
'DataGridViewPosts
|
|
||||||
'
|
|
||||||
Me.DataGridViewPosts.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize
|
|
||||||
Me.DataGridViewPosts.Dock = System.Windows.Forms.DockStyle.Fill
|
|
||||||
Me.DataGridViewPosts.Location = New System.Drawing.Point(0, 0)
|
|
||||||
Me.DataGridViewPosts.Name = "DataGridViewPosts"
|
|
||||||
Me.DataGridViewPosts.ReadOnly = True
|
|
||||||
Me.DataGridViewPosts.RowTemplate.Height = 25
|
|
||||||
Me.DataGridViewPosts.Size = New System.Drawing.Size(800, 450)
|
|
||||||
Me.DataGridViewPosts.TabIndex = 3
|
|
||||||
'
|
|
||||||
'PostsForm
|
|
||||||
'
|
|
||||||
Me.AutoScaleDimensions = New System.Drawing.SizeF(7.0!, 15.0!)
|
|
||||||
Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font
|
|
||||||
Me.ClientSize = New System.Drawing.Size(800, 450)
|
|
||||||
Me.Controls.Add(Me.DataGridViewPosts)
|
|
||||||
Me.Name = "PostsForm"
|
|
||||||
Me.ShowIcon = False
|
|
||||||
Me.ShowInTaskbar = False
|
|
||||||
Me.Text = "PostsForm"
|
|
||||||
CType(Me.DataGridViewPosts, System.ComponentModel.ISupportInitialize).EndInit()
|
|
||||||
Me.ResumeLayout(False)
|
|
||||||
|
|
||||||
End Sub
|
|
||||||
|
|
||||||
Friend WithEvents DataGridViewPosts As DataGridView
|
|
||||||
End Class
|
|
||||||
@@ -1,283 +0,0 @@
|
|||||||
Imports System.IO
|
|
||||||
Imports Newtonsoft.Json
|
|
||||||
Imports Newtonsoft.Json.Linq
|
|
||||||
|
|
||||||
Public Class StartForm
|
|
||||||
|
|
||||||
' Create the program's directory
|
|
||||||
' We will store information about the user and current machine in it
|
|
||||||
Public LicenseText As String = "MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2023 Richard Mwewa
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the ""Software""), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE."
|
|
||||||
Private Sub PathFinder()
|
|
||||||
Dim directoryPath As String = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RedditPostScrapingTool", "logs")
|
|
||||||
|
|
||||||
If Not Directory.Exists(directoryPath) Then
|
|
||||||
Directory.CreateDirectory(directoryPath)
|
|
||||||
Else
|
|
||||||
' DO NOTHING
|
|
||||||
End If
|
|
||||||
End Sub
|
|
||||||
|
|
||||||
|
|
||||||
Private Sub LicenseNotice()
|
|
||||||
MessageBox.Show(LicenseText, "License", MessageBoxButtons.OK, MessageBoxIcon.Information)
|
|
||||||
End Sub
|
|
||||||
|
|
||||||
|
|
||||||
' Create a file in C:\Users\<username>\AppData\Roaming\RedditPostScrapingTool, this will be used to determine
|
|
||||||
' Whether the program has been run before
|
|
||||||
' If it has not been run before, display the license notice
|
|
||||||
Private Sub LogFirstTimeLaunch()
|
|
||||||
Dim filePath As String = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RedditPostScrapingTool", "logs", "first_launch.log")
|
|
||||||
Dim textToWrite As String = $"
|
|
||||||
{My.Application.Info.AssemblyName}
|
|
||||||
-------------------------
|
|
||||||
|
|
||||||
|
|
||||||
User: {Environment.UserName}
|
|
||||||
Host: {Environment.MachineName}
|
|
||||||
OS: {Environment.OSVersion}
|
|
||||||
x64: {Environment.Is64BitOperatingSystem}
|
|
||||||
First launched on: {DateTime.Now}"
|
|
||||||
|
|
||||||
If Not File.Exists(filePath) Then
|
|
||||||
LicenseNotice()
|
|
||||||
File.WriteAllText(filePath, textToWrite)
|
|
||||||
Else
|
|
||||||
' DO NOTHING
|
|
||||||
End If
|
|
||||||
End Sub
|
|
||||||
|
|
||||||
|
|
||||||
' Check the current time
|
|
||||||
' add a dark background to the program if it's evening
|
|
||||||
' This is my way of implementing auto dark-mode (you could help if you know a better way :) )
|
|
||||||
Private Sub DarkModeProperties()
|
|
||||||
Dim currentHour As Integer = DateTime.Now.Hour
|
|
||||||
If currentHour >= 6 And currentHour < 18 Then
|
|
||||||
Me.BackColor = ColorTranslator.FromHtml("#FFFFFFFF")
|
|
||||||
|
|
||||||
ToolsToolStripMenuTools.ForeColor = ColorTranslator.FromHtml("#FF121212")
|
|
||||||
KeywordTextBox.BackColor = ColorTranslator.FromHtml("#FFFFFFFF")
|
|
||||||
KeywordTextBox.ForeColor = ColorTranslator.FromHtml("#FF121212")
|
|
||||||
|
|
||||||
SubredditTextBox.BackColor = ColorTranslator.FromHtml("#FFFFFFFF")
|
|
||||||
SubredditTextBox.ForeColor = ColorTranslator.FromHtml("#FF121212")
|
|
||||||
|
|
||||||
LimitNumericUpDown.BackColor = ColorTranslator.FromHtml("#FFFFFFFF")
|
|
||||||
LimitNumericUpDown.ForeColor = ColorTranslator.FromHtml("#FF121212")
|
|
||||||
|
|
||||||
LimitNumericUpDown.BackColor = ColorTranslator.FromHtml("#FFFFFFFF")
|
|
||||||
LimitNumericUpDown.ForeColor = ColorTranslator.FromHtml("#FF121212")
|
|
||||||
|
|
||||||
ListingComboBox.BackColor = ColorTranslator.FromHtml("#FFFFFFFF")
|
|
||||||
ListingComboBox.ForeColor = ColorTranslator.FromHtml("#FF121212")
|
|
||||||
|
|
||||||
TimeframeComboBox.BackColor = ColorTranslator.FromHtml("#FFFFFFFF")
|
|
||||||
TimeframeComboBox.ForeColor = ColorTranslator.FromHtml("#FF121212")
|
|
||||||
|
|
||||||
Label1.ForeColor = ColorTranslator.FromHtml("#FF121212")
|
|
||||||
Label2.ForeColor = ColorTranslator.FromHtml("#FF121212")
|
|
||||||
Label3.ForeColor = ColorTranslator.FromHtml("#FF121212")
|
|
||||||
Label4.ForeColor = ColorTranslator.FromHtml("#FF121212")
|
|
||||||
Label5.ForeColor = ColorTranslator.FromHtml("#FF121212")
|
|
||||||
Else
|
|
||||||
Me.BackColor = ColorTranslator.FromHtml("#FF121212")
|
|
||||||
|
|
||||||
ToolsToolStripMenuTools.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
|
|
||||||
KeywordTextBox.BackColor = ColorTranslator.FromHtml("#FF2E2E2E")
|
|
||||||
KeywordTextBox.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
|
|
||||||
|
|
||||||
SubredditTextBox.BackColor = ColorTranslator.FromHtml("#FF2E2E2E")
|
|
||||||
SubredditTextBox.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
|
|
||||||
|
|
||||||
LimitNumericUpDown.BackColor = ColorTranslator.FromHtml("#FF2E2E2E")
|
|
||||||
LimitNumericUpDown.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
|
|
||||||
LimitNumericUpDown.BackColor = ColorTranslator.FromHtml("#FF2E2E2E")
|
|
||||||
LimitNumericUpDown.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
|
|
||||||
|
|
||||||
ListingComboBox.BackColor = ColorTranslator.FromHtml("#FF2E2E2E")
|
|
||||||
ListingComboBox.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
|
|
||||||
|
|
||||||
TimeframeComboBox.BackColor = ColorTranslator.FromHtml("#FF2E2E2E")
|
|
||||||
TimeframeComboBox.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
|
|
||||||
|
|
||||||
Label1.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
|
|
||||||
Label2.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
|
|
||||||
Label3.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
|
|
||||||
Label4.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
|
|
||||||
Label5.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
|
|
||||||
End If
|
|
||||||
End Sub
|
|
||||||
|
|
||||||
|
|
||||||
Private Sub ScrapeButton_Click(sender As Object, e As EventArgs) Handles ScrapeButton.Click
|
|
||||||
Dim ApiHandler As New ApiHandler
|
|
||||||
Dim Keyword As String = KeywordTextBox.Text
|
|
||||||
Dim Subreddit As String = SubredditTextBox.Text
|
|
||||||
Dim Listing As String = ListingComboBox.Text.ToLower()
|
|
||||||
Dim Limit As Integer = LimitNumericUpDown.Value
|
|
||||||
Dim Timeframe As String = TimeframeComboBox.Text.ToLower()
|
|
||||||
Dim FoundPosts As Integer = 0
|
|
||||||
Dim TotalPosts As Integer = 0
|
|
||||||
|
|
||||||
' Clear the Columns and Rows before adding Items to them
|
|
||||||
PostsForm.DataGridViewPosts.Rows.Clear()
|
|
||||||
PostsForm.DataGridViewPosts.Columns.Clear()
|
|
||||||
|
|
||||||
PostsForm.DataGridViewPosts.Columns.Add("PostCount", "Post Number")
|
|
||||||
PostsForm.DataGridViewPosts.Columns.Add("PostAuthor", "Author")
|
|
||||||
PostsForm.DataGridViewPosts.Columns.Add("PostID", "ID")
|
|
||||||
PostsForm.DataGridViewPosts.Columns.Add("PostSubreddit", "Subreddit")
|
|
||||||
PostsForm.DataGridViewPosts.Columns.Add("SubredditVisibility", "Subreddit Visibility")
|
|
||||||
PostsForm.DataGridViewPosts.Columns.Add("PostThumbnail", "Thumbnail")
|
|
||||||
PostsForm.DataGridViewPosts.Columns.Add("PostIsNSFW", "NSFW")
|
|
||||||
PostsForm.DataGridViewPosts.Columns.Add("PostIsGilded", "Gilded")
|
|
||||||
PostsForm.DataGridViewPosts.Columns.Add("PostUpvotes", "Upvotes")
|
|
||||||
PostsForm.DataGridViewPosts.Columns.Add("PostUpvoteRatio", "Upvote Ratio")
|
|
||||||
PostsForm.DataGridViewPosts.Columns.Add("PostDownvotes", "Downvotes")
|
|
||||||
PostsForm.DataGridViewPosts.Columns.Add("PostAwards", "Awards")
|
|
||||||
PostsForm.DataGridViewPosts.Columns.Add("PostTopAward", "Top Award")
|
|
||||||
PostsForm.DataGridViewPosts.Columns.Add("PostIsCrosspostable", "Is Crosspostable?")
|
|
||||||
PostsForm.DataGridViewPosts.Columns.Add("PostScore", "Score")
|
|
||||||
PostsForm.DataGridViewPosts.Columns.Add("PostText", "Text")
|
|
||||||
PostsForm.DataGridViewPosts.Columns.Add("PostCategory", "Category")
|
|
||||||
PostsForm.DataGridViewPosts.Columns.Add("PostDomain", "Domain")
|
|
||||||
PostsForm.DataGridViewPosts.Columns.Add("PostPermalink", "Permalink")
|
|
||||||
PostsForm.DataGridViewPosts.Columns.Add("PostCreatedAt", "Created At")
|
|
||||||
PostsForm.DataGridViewPosts.Columns.Add("PostApprovedAt", "Approved At")
|
|
||||||
PostsForm.DataGridViewPosts.Columns.Add("PostApprovedBy", "Approved By")
|
|
||||||
|
|
||||||
If Limit > 100 Then
|
|
||||||
MessageBox.Show("Limit should not be over 100. Defaulting to 10", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning)
|
|
||||||
End If
|
|
||||||
|
|
||||||
If Listing = "" Then
|
|
||||||
Listing = "top"
|
|
||||||
End If
|
|
||||||
|
|
||||||
If Timeframe = "" Then
|
|
||||||
Timeframe = "all"
|
|
||||||
End If
|
|
||||||
|
|
||||||
If Keyword = "" Then
|
|
||||||
MessageBox.Show("Keyword should not be emtpy", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning)
|
|
||||||
ElseIf Subreddit = "" Then
|
|
||||||
MessageBox.Show("Subreddit should not be emtpy", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning)
|
|
||||||
Else
|
|
||||||
PostsForm.Text = $"Reddit Post Scraping Tool - {Keyword}"
|
|
||||||
Dim Posts As JObject = ApiHandler.ScrapeReddit(Subreddit, Listing, Limit, Timeframe)
|
|
||||||
For Each Post In Posts("data")("children")
|
|
||||||
TotalPosts += 1
|
|
||||||
If Post("data")("selftext").ToString.ToLower().Contains(KeywordTextBox.Text.ToLower()) Then
|
|
||||||
FoundPosts += 1
|
|
||||||
PostsForm.DataGridViewPosts.Rows.Add(TotalPosts, Post("data")("author"), Post("data")("id"), Post("data")("subreddit_name_prefixed"),
|
|
||||||
Post("data")("subreddit_type"), Post("data")("thumbnail"), Post("data")("over_18"), Post("data")("gilded"),
|
|
||||||
Post("data")("ups"), Post("data")("upvote_ratio"), Post("data")("downs"), Post("data")("total_awards_received"),
|
|
||||||
Post("data")("top_awarded_type"), Post("data")("is_crosspostable"), Post("data")("score"), Post("data")("selftext"),
|
|
||||||
Post("data")("category"), Post("data")("domain"), Post("data")("permalink"), Post("data")("created"),
|
|
||||||
Post("data")("approved_at_utc"), Post("data")("approved_by"))
|
|
||||||
End If
|
|
||||||
Next
|
|
||||||
|
|
||||||
'Don't show the results form if found posts are not greater than 0
|
|
||||||
If FoundPosts > 0 Then
|
|
||||||
MessageBox.Show($"Keyword `{Keyword}` was found in {FoundPosts}/" + Posts("data")("children").Count.ToString _
|
|
||||||
+ $" {Listing} posts from r/{Subreddit}", "Found", MessageBoxButtons.OK, MessageBoxIcon.Information)
|
|
||||||
PostsForm.Show()
|
|
||||||
Else
|
|
||||||
MessageBox.Show($"Keyword `{Keyword}` was not found in either one of the " + Posts("data")("children").Count.ToString _
|
|
||||||
+ $" {Listing} posts from r/{Subreddit}", "Not Found", MessageBoxButtons.OK, MessageBoxIcon.Warning)
|
|
||||||
End If
|
|
||||||
|
|
||||||
|
|
||||||
If JSONToolStripMenuItem.Checked Then
|
|
||||||
Dim saveFileDialog As New SaveFileDialog()
|
|
||||||
|
|
||||||
saveFileDialog.Filter = "JSON files (*.json)|*.json"
|
|
||||||
saveFileDialog.Title = "Save posts to JSON"
|
|
||||||
|
|
||||||
If saveFileDialog.ShowDialog() = DialogResult.OK Then
|
|
||||||
Dim fileName As String = saveFileDialog.FileName
|
|
||||||
Dim serializerSettings As New JsonSerializerSettings()
|
|
||||||
serializerSettings.Formatting = Formatting.Indented
|
|
||||||
Dim json As String = JsonConvert.SerializeObject(Posts("data"), serializerSettings)
|
|
||||||
|
|
||||||
System.IO.File.WriteAllText(fileName, json)
|
|
||||||
|
|
||||||
MessageBox.Show($"Results saved to {fileName} successfully!", "Success", MessageBoxButtons.OK, MessageBoxIcon.Information)
|
|
||||||
End If
|
|
||||||
End If
|
|
||||||
End If
|
|
||||||
End Sub
|
|
||||||
|
|
||||||
' StartForm load event
|
|
||||||
Private Sub StartForm_Load(sender As Object, e As EventArgs) Handles MyBase.Load
|
|
||||||
PathFinder()
|
|
||||||
LogFirstTimeLaunch()
|
|
||||||
DarkModeProperties()
|
|
||||||
ToolsToolStripMenuTools.Text = Environment.UserName
|
|
||||||
Me.Text = $"{My.Application.Info.AssemblyName} v{My.Application.Info.Version}"
|
|
||||||
End Sub
|
|
||||||
|
|
||||||
Private Sub AboutToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles AboutToolStripMenuItem.Click
|
|
||||||
AboutForm.ShowDialog()
|
|
||||||
End Sub
|
|
||||||
|
|
||||||
Private Sub QuitToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles QuitToolStripMenuItem.Click
|
|
||||||
Dim result As DialogResult = MessageBox.Show("This will close the program, continue?", "Quit", MessageBoxButtons.YesNo, MessageBoxIcon.Question)
|
|
||||||
If result = DialogResult.Yes Then
|
|
||||||
Me.Close()
|
|
||||||
End If
|
|
||||||
End Sub
|
|
||||||
|
|
||||||
Private Sub DeveloperToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles DeveloperToolStripMenuItem.Click
|
|
||||||
DeveloperForm.ShowDialog()
|
|
||||||
End Sub
|
|
||||||
|
|
||||||
Private Sub ChekUpdatesToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles ChekUpdatesToolStripMenuItem.Click
|
|
||||||
Dim ApiHandler As New ApiHandler()
|
|
||||||
Dim data As JObject = ApiHandler.CheckUpdates()
|
|
||||||
If data("tag_name").ToString = $"{My.Application.Info.Version}" Then
|
|
||||||
MessageBox.Show($"You're running the current version v{My.Application.Info.Version} of {My.Application.Info.ProductName}. Check again soon! :)", "Information", MessageBoxButtons.OK, MessageBoxIcon.Information)
|
|
||||||
Else
|
|
||||||
Dim confirm As DialogResult = MessageBox.Show($"A new version v{data("tag_name")} of {My.Application.Info.ProductName} is availble, would you like to get it?
|
|
||||||
|
|
||||||
What's new in v{data("tag_name")}?
|
|
||||||
{data("body")}
|
|
||||||
", "Update", MessageBoxButtons.YesNo, MessageBoxIcon.Question)
|
|
||||||
If confirm = DialogResult.Yes Then
|
|
||||||
Shell($"cmd /c start https://github.com/bellingcat/reddit-post-scraping-tool/releases/tag/{data("tag_name")}")
|
|
||||||
End If
|
|
||||||
End If
|
|
||||||
|
|
||||||
End Sub
|
|
||||||
|
|
||||||
Private Sub ToolsToolStripMenuTools_Click(sender As Object, e As EventArgs) Handles ToolsToolStripMenuTools.Click
|
|
||||||
ToolsToolStripMenuTools.ForeColor = ColorTranslator.FromHtml("#FF121212")
|
|
||||||
End Sub
|
|
||||||
|
|
||||||
Private Sub ToolsToolStripMenuTools_DropDownClosed(sender As Object, e As EventArgs) Handles ToolsToolStripMenuTools.DropDownClosed
|
|
||||||
ToolsToolStripMenuTools.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
|
|
||||||
End Sub
|
|
||||||
|
|
||||||
End Class
|
|
||||||
@@ -2,10 +2,13 @@
|
|||||||
requires = ["setuptools", "setuptools-scm"]
|
requires = ["setuptools", "setuptools-scm"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[tool.setuptools]
|
||||||
|
packages = ["rpst"]
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "reddit-post-scraping-tool"
|
name = "reddit-post-scraping-tool"
|
||||||
version = "1.3.0.1"
|
version = "1.4.0.0"
|
||||||
description = "Given a subreddit name and a keyword, this program returns all top (by default) posts that contain the specified word."
|
description = "Given a subreddit name and a keyword, RPST returns all top (by default) posts that contain the specified keyword."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
license = {file = "LICENSE"}
|
license = {file = "LICENSE"}
|
||||||
@@ -27,9 +30,9 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
homepage = "https://github.com/bellingcat/reddit-post-scraping-tool"
|
homepage = "https://github.com/bellingcat"
|
||||||
documentation = "https://github.com/bellingcat/reddit-post-scraping-tool/wiki"
|
documentation = "https://github.com/bellingcat/reddit-post-scraping-tool/wiki"
|
||||||
repository = "https://github.com/bellingcat/reddit-post-scraping-tool.git"
|
repository = "https://github.com/bellingcat/reddit-post-scraping-tool.git"
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
reddit_post_scraping_tool = "reddit_post_scraping_tool.main:main"
|
rpst = "rpst.__main:run"
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
from reddit_post_scraping_tool.reddit_post_scraping_tool import *
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
try:
|
|
||||||
check_updates("1.3.0.1")
|
|
||||||
reddit_post_scraper()
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
log.warning(f"User interruption detected.")
|
|
||||||
except Exception as e:
|
|
||||||
log.error(e)
|
|
||||||
finally:
|
|
||||||
log.info(f'Finished in {datetime.now() - start_time} seconds.')
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
import logging
|
|
||||||
import argparse
|
|
||||||
import requests
|
|
||||||
from rich.tree import Tree
|
|
||||||
from datetime import datetime
|
|
||||||
from rich import print as xprint
|
|
||||||
from rich.markdown import Markdown
|
|
||||||
from rich.logging import RichHandler
|
|
||||||
|
|
||||||
|
|
||||||
start_time = datetime.now()
|
|
||||||
logging.basicConfig(level="NOTSET", format="%(message)s", handlers=[RichHandler(markup=True, log_time_format='[%H:%M:%S%p]')])
|
|
||||||
log = logging.getLogger("rich")
|
|
||||||
|
|
||||||
|
|
||||||
# Check if the remote tag_name from the latest release matches the one in the program
|
|
||||||
# if it does, it means the program is up-to-date.
|
|
||||||
# If it doesn't match, notify the user about a new release
|
|
||||||
def check_updates(version_tag):
|
|
||||||
response = requests.get("https://api.github.com/repos/bellingcat/reddit-post-scraping-tool/releases/latest").json()
|
|
||||||
if response['tag_name'] == version_tag:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
raw_release_notes = response['body']
|
|
||||||
markdown_release_notes = Markdown(raw_release_notes)
|
|
||||||
log.info(f"A new release of reddit-post-scraping-tool is available ({response['tag_name']}). Run 'pip install --upgrade reddit-post-scraping-tool' to get the updates.")
|
|
||||||
xprint(markdown_release_notes)
|
|
||||||
|
|
||||||
|
|
||||||
# Getting posts
|
|
||||||
def get_posts(post):
|
|
||||||
post_data = {'Author': post['data']['author'],
|
|
||||||
'ID': post['data']['id'],
|
|
||||||
'Subreddit': post["data"]["subreddit_name_prefixed"],
|
|
||||||
'Visibility': post['data']['subreddit_type'],
|
|
||||||
# 'Author': post["data"]["author_fullname"],
|
|
||||||
'Thumbnail': post["data"]["thumbnail"],
|
|
||||||
# 'Flair': post["data"]["link_flair_text"],
|
|
||||||
'NSFW': post['data']['over_18'],
|
|
||||||
'Gilded': post['data']['gilded'],
|
|
||||||
'Upvotes': post["data"]["ups"],
|
|
||||||
'Upvote ratio': post["data"]["upvote_ratio"],
|
|
||||||
'Downvotes': post["data"]["downs"],
|
|
||||||
'Awards': post["data"]["total_awards_received"],
|
|
||||||
'Top award': post['data']['top_awarded_type'],
|
|
||||||
'Is crosspostable?': post['data']['is_crosspostable'],
|
|
||||||
'Score': post["data"]["score"],
|
|
||||||
'Category': post['data']['category'],
|
|
||||||
'Domain': post["data"]["domain"],
|
|
||||||
'Created': post['data']['created'],
|
|
||||||
'Approved at': post['data']['approved_at_utc'],
|
|
||||||
'Approved by': post['data']['approved_by'], }
|
|
||||||
|
|
||||||
post_tree = Tree("\n" + post['data']['title'])
|
|
||||||
for post_key, post_value in post_data.items():
|
|
||||||
post_tree.add(f"{post_key}: {post_value}")
|
|
||||||
xprint(post_tree)
|
|
||||||
print(post['data']['selftext'] + "\n")
|
|
||||||
|
|
||||||
|
|
||||||
def reddit_post_scraper():
|
|
||||||
session = requests.session()
|
|
||||||
session.headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15'}
|
|
||||||
response = session.get(f'https://reddit.com/r/{args.subreddit}/{args.listing}.json?limit={args.limit}&t={args.timeframe}').json()
|
|
||||||
found_posts = 0
|
|
||||||
for post in response['data']['children']:
|
|
||||||
if args.keyword.lower() in post['data']['selftext'] or args.keyword.lower() in post['data']['title']:
|
|
||||||
found_posts += 1
|
|
||||||
get_posts(post)
|
|
||||||
|
|
||||||
log.info(f"Keyword ('{args.keyword}') was found in {found_posts}/{len(response['data']['children'])} {args.listing} posts from r/{args.subreddit}.")
|
|
||||||
|
|
||||||
|
|
||||||
def create_parser():
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description=f'reddit-post-scraping-tool — by Richard Mwewa | https://about.me/rly0nheart',
|
|
||||||
epilog=f'Given a subreddit name and a keyword, this program returns all top (by default) posts that contain the specified word. ')
|
|
||||||
parser.add_argument('-k', '--keyword', help='kewyword', required=True)
|
|
||||||
parser.add_argument('-s', '--subreddit', help='subreddit', required=True)
|
|
||||||
parser.add_argument('-c', '--limit', help='results limit (1-100) (default: %(default)s)', default=10, type=int)
|
|
||||||
parser.add_argument('-l', '--listing', default='top', const='top', nargs='?',
|
|
||||||
choices=['controversial', 'hot', 'best', 'new', 'rising'],
|
|
||||||
help='listings: controversial, hot, best, new, rising (default: %(default)s)')
|
|
||||||
parser.add_argument('-t', '--timeframe', default='all', const='all', nargs='?',
|
|
||||||
choices=['hour', 'day', 'week', 'month', 'year'],
|
|
||||||
help='timeframe: hour, day, week, month, year (default: %(default)s)')
|
|
||||||
return parser
|
|
||||||
|
|
||||||
|
|
||||||
_parser = create_parser()
|
|
||||||
args = _parser.parse_args()
|
|
||||||
29
rpst/__main.py
Normal file
29
rpst/__main.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from rpst.__rpst import log, get_posts, check_updates, create_parser
|
||||||
|
|
||||||
|
|
||||||
|
def run():
|
||||||
|
"""
|
||||||
|
Main entry point for the program. It creates a parser, parses the command line arguments,
|
||||||
|
checks for updates, gets posts, and handles any exceptions that occur during the execution.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Create a parser and parse the command line arguments
|
||||||
|
parser = create_parser()
|
||||||
|
arguments = parser.parse_args()
|
||||||
|
|
||||||
|
# Record the start time
|
||||||
|
start_time = datetime.now()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Check for updates
|
||||||
|
check_updates(version_tag="1.4.0.0")
|
||||||
|
|
||||||
|
# Get posts with the provided/parsed arguments
|
||||||
|
get_posts(arguments=arguments)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
log.warning("User interruption detected.")
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"An error occurred: {e}")
|
||||||
|
finally:
|
||||||
|
log.info(f'Finished in {datetime.now() - start_time} seconds.')
|
||||||
201
rpst/__rpst_.py
Normal file
201
rpst/__rpst_.py
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import argparse
|
||||||
|
import requests
|
||||||
|
from rich.tree import Tree
|
||||||
|
from rich import print as xprint
|
||||||
|
from rich.markdown import Markdown
|
||||||
|
from rich.logging import RichHandler
|
||||||
|
|
||||||
|
|
||||||
|
def write_post_data(post_data: dict, filename: str):
|
||||||
|
"""
|
||||||
|
Writes post data to a specified JSON file.
|
||||||
|
|
||||||
|
:param post_data: A dictionary containing post data.
|
||||||
|
:param filename: The name of the file to which post data will be written.
|
||||||
|
"""
|
||||||
|
# Write the data to a JSON file
|
||||||
|
with open(filename + ".json", 'a') as file:
|
||||||
|
file.write(json.dumps(post_data))
|
||||||
|
file.write('\n') # write a newline to separate posts
|
||||||
|
|
||||||
|
log.info(f"Post data written to '{file.name}'")
|
||||||
|
|
||||||
|
|
||||||
|
def check_updates(version_tag: str):
|
||||||
|
"""
|
||||||
|
This function checks if there's a new release of a project on GitHub. If there is, it logs an
|
||||||
|
information message and prints the release notes.
|
||||||
|
|
||||||
|
:param version_tag: A string representing the current version of the project.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Make a GET request to the GitHub API to get the latest release of the project
|
||||||
|
response = requests.get("https://api.github.com/repos/bellingcat/reddit-post-scraping-tool/releases/latest").json()
|
||||||
|
|
||||||
|
# Check if the latest release's tag matches the current version tag
|
||||||
|
if response['tag_name'] != version_tag:
|
||||||
|
|
||||||
|
# If not, convert the release notes from Markdown to HTML
|
||||||
|
raw_release_notes = response['body']
|
||||||
|
markdown_release_notes = Markdown(raw_release_notes)
|
||||||
|
|
||||||
|
# Log an info message about the new release
|
||||||
|
log.info(
|
||||||
|
f"A new release of RPST is available ({response['tag_name']}). "
|
||||||
|
f"Run 'pip install --upgrade reddit-post-scraping-tool' to get the updates."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Print the release notes
|
||||||
|
xprint(markdown_release_notes)
|
||||||
|
|
||||||
|
|
||||||
|
def format_post_data(post: dict, keyword: str, output: bool):
|
||||||
|
"""
|
||||||
|
This function extracts relevant data from a Reddit post and displays it in a tree structure,
|
||||||
|
followed by the post's selftext.
|
||||||
|
|
||||||
|
:param post: A dictionary containing the data of a Reddit post.
|
||||||
|
:param keyword: The keyword that is used to find posts, in his case gets uses as the filename.
|
||||||
|
:param output: If specified, all found posts will be written to a json file.
|
||||||
|
"""
|
||||||
|
# Define the data to extract from the post
|
||||||
|
post_data = {
|
||||||
|
'Author': post['data']['author'],
|
||||||
|
'ID': post['data']['id'],
|
||||||
|
'Subreddit': post["data"]["subreddit_name_prefixed"],
|
||||||
|
'Visibility': post['data']['subreddit_type'],
|
||||||
|
'Thumbnail': post["data"]["thumbnail"],
|
||||||
|
'NSFW': post['data']['over_18'],
|
||||||
|
'Gilded': post['data']['gilded'],
|
||||||
|
'Upvotes': post["data"]["ups"],
|
||||||
|
'Upvote ratio': post["data"]["upvote_ratio"],
|
||||||
|
'Downvotes': post["data"]["downs"],
|
||||||
|
'Awards': post["data"]["total_awards_received"],
|
||||||
|
'Top award': post['data']['top_awarded_type'],
|
||||||
|
'Is crosspostable?': post['data']['is_crosspostable'],
|
||||||
|
'Score': post["data"]["score"],
|
||||||
|
'Category': post['data']['category'],
|
||||||
|
'Domain': post["data"]["domain"],
|
||||||
|
'Created': post['data']['created'],
|
||||||
|
'Approved at': post['data']['approved_at_utc'],
|
||||||
|
'Approved by': post['data']['approved_by'],
|
||||||
|
}
|
||||||
|
if output:
|
||||||
|
write_post_data(filename=keyword, post_data=post_data)
|
||||||
|
# Create a tree structure with the post's title as the root
|
||||||
|
post_tree = Tree("\n" + post['data']['title'])
|
||||||
|
|
||||||
|
# Add each piece of extracted data as a branch of the tree
|
||||||
|
for post_key, post_value in post_data.items():
|
||||||
|
post_tree.add(f"{post_key}: {post_value}")
|
||||||
|
|
||||||
|
# Print the tree structure
|
||||||
|
xprint(post_tree)
|
||||||
|
|
||||||
|
# Print the post's selftext
|
||||||
|
print(post['data']['selftext'] + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def get_posts(arguments: argparse):
|
||||||
|
"""
|
||||||
|
Scrapes a given subreddit for posts that contain a specified keyword.
|
||||||
|
The search is limited by the number of posts and timeframe specified. The results are either
|
||||||
|
printed to the console or saved to a specified file, based on the 'output' argument.
|
||||||
|
|
||||||
|
:param arguments: Namespace object from argparse.
|
||||||
|
|
||||||
|
Expected Object Attributes
|
||||||
|
--------------------------
|
||||||
|
- keyword: The keyword to search for in the posts.
|
||||||
|
- subreddit: The subreddit to scrape.
|
||||||
|
- listing: The type of posts to scrape. This could be 'hot', 'new', etc.
|
||||||
|
- timeframe: The timeframe from which to scrape posts. This could be 'day', 'week', etc.
|
||||||
|
- limit: The maximum number of posts to scrape.
|
||||||
|
- json: If specified, all found posts will be written to a json file.
|
||||||
|
|
||||||
|
Also logs the number of posts in which the keyword was found.
|
||||||
|
"""
|
||||||
|
keyword = arguments.keyword
|
||||||
|
subreddit = arguments.subreddit
|
||||||
|
listing = arguments.listing
|
||||||
|
timeframe = arguments.timeframe
|
||||||
|
limit = arguments.limit
|
||||||
|
json_output = arguments.json
|
||||||
|
|
||||||
|
# Start a new session
|
||||||
|
session = requests.session()
|
||||||
|
# Set the User-Agent to mimic a Safari browser on a Mac
|
||||||
|
session.headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, '
|
||||||
|
'like Gecko) Version/14.1.1 Safari/605.1.15'}
|
||||||
|
|
||||||
|
# Send a GET request to the specified subreddit and listing,
|
||||||
|
# limiting the response by the specified limit and timeframe
|
||||||
|
response = session.get(f'https://reddit.com/r/{subreddit}/{listing}'
|
||||||
|
f'.json?limit={limit}&t={timeframe}').json()
|
||||||
|
|
||||||
|
# Initialize a counter for the number of posts found that contain the keyword
|
||||||
|
found_posts = 0
|
||||||
|
|
||||||
|
# Loop through each post in the response
|
||||||
|
for post in response['data']['children']:
|
||||||
|
# If the keyword is found in the post's selftext or title, increment the counter and process the post
|
||||||
|
if keyword.lower() in post['data']['selftext'] or keyword.lower() in post['data']['title']:
|
||||||
|
found_posts += 1
|
||||||
|
format_post_data(post=post, keyword=keyword, output=json_output)
|
||||||
|
|
||||||
|
# Log the number of posts in which the keyword was found
|
||||||
|
log.info(f"Keyword ('{keyword}') was found in {found_posts}/{len(response['data']['children'])} "
|
||||||
|
f"{listing} posts from r/{subreddit}.")
|
||||||
|
|
||||||
|
|
||||||
|
def create_parser():
|
||||||
|
"""
|
||||||
|
Creates and configures an argument parser for the command line arguments.
|
||||||
|
|
||||||
|
:return: A configured argparse.ArgumentParser object ready to parse the command line arguments.
|
||||||
|
"""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='RPST: Reddit Post Scraping Tool —by Richard Mwewa | https://about.me/rly0nheart',
|
||||||
|
epilog='Given a subreddit name and a keyword, '
|
||||||
|
'RPST returns all top (by default) posts that contain the specified keyword.'
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument('-k', '--keyword', help='The keyword to search for in the posts.', required=True)
|
||||||
|
parser.add_argument('-s', '--subreddit', help='The subreddit to scrape.', required=True)
|
||||||
|
parser.add_argument(
|
||||||
|
'-c', '--limit',
|
||||||
|
help='The maximum number of posts to scrape (1-100). (default: %(default)s)',
|
||||||
|
default=10,
|
||||||
|
type=int,
|
||||||
|
choices=range(1, 101) # This enforces that the limit must be between 1 and 100 inclusive.
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-l', '--listing',
|
||||||
|
default='top',
|
||||||
|
const='top',
|
||||||
|
nargs='?',
|
||||||
|
choices=['controversial', 'hot', 'best', 'new', 'rising'],
|
||||||
|
help='The type of posts to scrape (default: %(default)s)'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-t', '--timeframe',
|
||||||
|
default='all',
|
||||||
|
const='all',
|
||||||
|
nargs='?',
|
||||||
|
choices=['hour', 'day', 'week', 'month', 'year', 'all'],
|
||||||
|
help='The timeframe from which to scrape posts (default: %(default)s)'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-j', '--json',
|
||||||
|
help='Write all found posts to a json file.',
|
||||||
|
action='store_true'
|
||||||
|
)
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
logging.basicConfig(level="NOTSET", format="%(message)s",
|
||||||
|
handlers=[RichHandler(markup=True, log_time_format='[%H:%M:%S%p]')])
|
||||||
|
log = logging.getLogger("rich")
|
||||||
Reference in New Issue
Block a user