30
README.md
@@ -1,42 +1,52 @@
|
||||
# Note
|
||||
> ## Use [Knew Karma](https://github.com/bellingcat/knewkarma) for more advanced and improved features.
|
||||

|
||||
|
||||
## Note
|
||||
|
||||
> Use [Knew Karma](https://github.com/bellingcat/knewkarma) for more advanced and improved features.
|
||||
|
||||
# RPST (Reddit Post Scraping Tool)
|
||||
Retrieve **Reddit** posts that contain the specified keyword from a specified subreddit.
|
||||
|
||||
Retrieve **Reddit** posts that contain the specified **keyword** from a specified **subreddit**.
|
||||
|
||||
[](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] Dark mode (*Right-click>Settings>Dark Mode*).
|
||||
- [x] Saves results to a JSON/CSV file (*Right-click>Settings>Save posts>to JSON/to CSV*).
|
||||
- [x] Logs errors to a file.
|
||||
- [x] In-App feature to check for Updates.
|
||||
|
||||
## *CLI*
|
||||
|
||||
- [x] Saves results to JSON (*specifiy* `--json`).
|
||||
- [x] Saves results to CSV (*specify* `--csv`).
|
||||
- [x] Automatically checks for new updates, and notifies user if updates were found.
|
||||
|
||||
# 📃 TODO
|
||||
|
||||
## *GUI*
|
||||
|
||||
- [ ] Make it installable with a setup.exe/setup.msi file.
|
||||
|
||||
# 🖥️ Tested environments
|
||||
|
||||
## *GUI*
|
||||
|
||||
- [x] Microsoft Windows 11
|
||||
|
||||
## *CLI*
|
||||
|
||||
- [x] Android Termux
|
||||
- [x] Microsoft Windows 11
|
||||
- [x] Ubuntu 22.04 - latest versions
|
||||
|
||||
# 📖 Wiki
|
||||
[Refer to the Wiki](https://github.com/bellingcat/reddit-post-scraping-tool/wiki) for installation instructions, in addition to all other documentation.
|
||||
# 📖 Documentation
|
||||
|
||||
[Refer to the Wiki](https://github.com/bellingcat/reddit-post-scraping-tool/wiki) for installation instructions, in
|
||||
addition to all other documentation.
|
||||
|
||||
# 🖼️ Screenshots
|
||||
You can view a collection of screenshots for both the *CLI* and *GUI* [here](https://github.com/bellingcat/reddit-post-scraping-tool/tree/master/images)
|
||||
***
|
||||
<a href="https://www.buymeacoffee.com/_rly0nheart"><img src="https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=&slug=_rly0nheart&button_colour=40DCA5&font_colour=ffffff&font_family=Comic&outline_colour=000000&coffee_colour=FFDD00" /></a>
|
||||
|
||||

|
||||
[](https://about.me/rly0nheart)
|
||||
|
||||
@@ -5,6 +5,8 @@ VisualStudioVersion = 17.4.33213.308
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{778DAE3C-4631-46EA-AA77-85C1314464D9}") = "RPST", "RPST\RPST.vbproj", "{46C2541E-6F65-461A-A479-F65D445C36EA}"
|
||||
EndProject
|
||||
Project("{54435603-DBB4-11D2-8724-00A0C9A8B90C}") = "RPSTSetup", "RPSTSetup\RPSTSetup.vdproj", "{7D89A26E-2D54-4BB7-B9C4-E1382E657DEA}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -15,6 +17,8 @@ Global
|
||||
{46C2541E-6F65-461A-A479-F65D445C36EA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{46C2541E-6F65-461A-A479-F65D445C36EA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{46C2541E-6F65-461A-A479-F65D445C36EA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{7D89A26E-2D54-4BB7-B9C4-E1382E657DEA}.Debug|Any CPU.ActiveCfg = Debug
|
||||
{7D89A26E-2D54-4BB7-B9C4-E1382E657DEA}.Release|Any CPU.ActiveCfg = Release
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
259
RPST GUI/RPST/AboutBox.Designer.vb
generated
@@ -1,259 +0,0 @@
|
||||
<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()> _
|
||||
Partial Class AboutBox
|
||||
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(AboutBox))
|
||||
PictureBoxLogo = New PictureBox()
|
||||
LabelProgramName = New Label()
|
||||
LabelDescription = New Label()
|
||||
TabControl1 = New TabControl()
|
||||
TabPageAbout = New TabPage()
|
||||
LabelCopyright = New Label()
|
||||
LinkLabelLicense = New LinkLabel()
|
||||
LinkLabelReadtheWiki = New LinkLabel()
|
||||
TabPageAuthor = New TabPage()
|
||||
LinkLabelEmail = New LinkLabel()
|
||||
LinkLabelBMC = New LinkLabel()
|
||||
LinkLabelAboutMe = New LinkLabel()
|
||||
LabelAuthor = New Label()
|
||||
LabelVersion = New Label()
|
||||
ButtonClose = New Button()
|
||||
CType(PictureBoxLogo, ComponentModel.ISupportInitialize).BeginInit()
|
||||
TabControl1.SuspendLayout()
|
||||
TabPageAbout.SuspendLayout()
|
||||
TabPageAuthor.SuspendLayout()
|
||||
SuspendLayout()
|
||||
'
|
||||
' PictureBoxLogo
|
||||
'
|
||||
PictureBoxLogo.BackColor = Color.Transparent
|
||||
PictureBoxLogo.Image = CType(resources.GetObject("PictureBoxLogo.Image"), Image)
|
||||
PictureBoxLogo.Location = New Point(12, 12)
|
||||
PictureBoxLogo.Name = "PictureBoxLogo"
|
||||
PictureBoxLogo.Size = New Size(62, 64)
|
||||
PictureBoxLogo.SizeMode = PictureBoxSizeMode.StretchImage
|
||||
PictureBoxLogo.TabIndex = 0
|
||||
PictureBoxLogo.TabStop = False
|
||||
'
|
||||
' LabelProgramName
|
||||
'
|
||||
LabelProgramName.AutoSize = True
|
||||
LabelProgramName.Font = New Font("Segoe UI Semibold", 9.75F, FontStyle.Bold, GraphicsUnit.Point)
|
||||
LabelProgramName.ForeColor = SystemColors.ControlText
|
||||
LabelProgramName.Location = New Point(80, 33)
|
||||
LabelProgramName.Name = "LabelProgramName"
|
||||
LabelProgramName.Size = New Size(44, 17)
|
||||
LabelProgramName.TabIndex = 3
|
||||
LabelProgramName.Text = "Name"
|
||||
'
|
||||
' LabelDescription
|
||||
'
|
||||
LabelDescription.AutoSize = True
|
||||
LabelDescription.Font = New Font("Segoe UI", 9F, FontStyle.Regular, GraphicsUnit.Point)
|
||||
LabelDescription.ForeColor = SystemColors.ControlText
|
||||
LabelDescription.Location = New Point(6, 7)
|
||||
LabelDescription.Name = "LabelDescription"
|
||||
LabelDescription.Size = New Size(67, 15)
|
||||
LabelDescription.TabIndex = 4
|
||||
LabelDescription.Text = "Description"
|
||||
'
|
||||
' TabControl1
|
||||
'
|
||||
TabControl1.Controls.Add(TabPageAbout)
|
||||
TabControl1.Controls.Add(TabPageAuthor)
|
||||
TabControl1.Font = New Font("Segoe UI", 9F, FontStyle.Regular, GraphicsUnit.Point)
|
||||
TabControl1.Location = New Point(12, 91)
|
||||
TabControl1.Name = "TabControl1"
|
||||
TabControl1.SelectedIndex = 0
|
||||
TabControl1.Size = New Size(322, 152)
|
||||
TabControl1.TabIndex = 8
|
||||
'
|
||||
' TabPageAbout
|
||||
'
|
||||
TabPageAbout.BackColor = Color.Transparent
|
||||
TabPageAbout.Controls.Add(LabelCopyright)
|
||||
TabPageAbout.Controls.Add(LinkLabelLicense)
|
||||
TabPageAbout.Controls.Add(LabelDescription)
|
||||
TabPageAbout.Controls.Add(LinkLabelReadtheWiki)
|
||||
TabPageAbout.Font = New Font("Segoe UI Semibold", 9F, FontStyle.Bold, GraphicsUnit.Point)
|
||||
TabPageAbout.Location = New Point(4, 24)
|
||||
TabPageAbout.Name = "TabPageAbout"
|
||||
TabPageAbout.Padding = New Padding(3)
|
||||
TabPageAbout.Size = New Size(314, 124)
|
||||
TabPageAbout.TabIndex = 0
|
||||
TabPageAbout.Text = "About"
|
||||
'
|
||||
' LabelCopyright
|
||||
'
|
||||
LabelCopyright.AutoSize = True
|
||||
LabelCopyright.Font = New Font("Segoe UI", 9F, FontStyle.Regular, GraphicsUnit.Point)
|
||||
LabelCopyright.Location = New Point(6, 97)
|
||||
LabelCopyright.Name = "LabelCopyright"
|
||||
LabelCopyright.Size = New Size(60, 15)
|
||||
LabelCopyright.TabIndex = 7
|
||||
LabelCopyright.Text = "Copyright"
|
||||
'
|
||||
' LinkLabelLicense
|
||||
'
|
||||
LinkLabelLicense.AutoSize = True
|
||||
LinkLabelLicense.Font = New Font("Segoe UI", 9F, FontStyle.Regular, GraphicsUnit.Point)
|
||||
LinkLabelLicense.Location = New Point(6, 52)
|
||||
LinkLabelLicense.Name = "LinkLabelLicense"
|
||||
LinkLabelLicense.Size = New Size(84, 15)
|
||||
LinkLabelLicense.TabIndex = 5
|
||||
LinkLabelLicense.TabStop = True
|
||||
LinkLabelLicense.Text = "🗒️ MIT License"
|
||||
'
|
||||
' LinkLabelReadtheWiki
|
||||
'
|
||||
LinkLabelReadtheWiki.AutoSize = True
|
||||
LinkLabelReadtheWiki.Font = New Font("Segoe UI", 9F, FontStyle.Regular, GraphicsUnit.Point)
|
||||
LinkLabelReadtheWiki.Location = New Point(6, 74)
|
||||
LinkLabelReadtheWiki.Name = "LinkLabelReadtheWiki"
|
||||
LinkLabelReadtheWiki.Size = New Size(94, 15)
|
||||
LinkLabelReadtheWiki.TabIndex = 6
|
||||
LinkLabelReadtheWiki.TabStop = True
|
||||
LinkLabelReadtheWiki.Text = "📖 Read the Wiki"
|
||||
'
|
||||
' TabPageAuthor
|
||||
'
|
||||
TabPageAuthor.BackColor = Color.Transparent
|
||||
TabPageAuthor.Controls.Add(LinkLabelEmail)
|
||||
TabPageAuthor.Controls.Add(LinkLabelBMC)
|
||||
TabPageAuthor.Controls.Add(LinkLabelAboutMe)
|
||||
TabPageAuthor.Controls.Add(LabelAuthor)
|
||||
TabPageAuthor.ForeColor = SystemColors.ControlText
|
||||
TabPageAuthor.Location = New Point(4, 24)
|
||||
TabPageAuthor.Name = "TabPageAuthor"
|
||||
TabPageAuthor.Padding = New Padding(3)
|
||||
TabPageAuthor.Size = New Size(314, 124)
|
||||
TabPageAuthor.TabIndex = 1
|
||||
TabPageAuthor.Text = "Author"
|
||||
'
|
||||
' LinkLabelEmail
|
||||
'
|
||||
LinkLabelEmail.AutoSize = True
|
||||
LinkLabelEmail.Location = New Point(6, 89)
|
||||
LinkLabelEmail.Name = "LinkLabelEmail"
|
||||
LinkLabelEmail.Size = New Size(51, 15)
|
||||
LinkLabelEmail.TabIndex = 3
|
||||
LinkLabelEmail.TabStop = True
|
||||
LinkLabelEmail.Text = "📧 Email"
|
||||
'
|
||||
' LinkLabelBMC
|
||||
'
|
||||
LinkLabelBMC.AutoSize = True
|
||||
LinkLabelBMC.Location = New Point(3, 66)
|
||||
LinkLabelBMC.Name = "LinkLabelBMC"
|
||||
LinkLabelBMC.Size = New Size(111, 15)
|
||||
LinkLabelBMC.TabIndex = 2
|
||||
LinkLabelBMC.TabStop = True
|
||||
LinkLabelBMC.Text = "🍵 Buy Me A Coffee"
|
||||
'
|
||||
' LinkLabelAboutMe
|
||||
'
|
||||
LinkLabelAboutMe.AutoSize = True
|
||||
LinkLabelAboutMe.Location = New Point(6, 43)
|
||||
LinkLabelAboutMe.Name = "LinkLabelAboutMe"
|
||||
LinkLabelAboutMe.Size = New Size(75, 15)
|
||||
LinkLabelAboutMe.TabIndex = 1
|
||||
LinkLabelAboutMe.TabStop = True
|
||||
LinkLabelAboutMe.Text = "🔗 About.me"
|
||||
'
|
||||
' LabelAuthor
|
||||
'
|
||||
LabelAuthor.AutoSize = True
|
||||
LabelAuthor.Font = New Font("Segoe UI", 9F, FontStyle.Bold, GraphicsUnit.Point)
|
||||
LabelAuthor.Location = New Point(6, 15)
|
||||
LabelAuthor.Name = "LabelAuthor"
|
||||
LabelAuthor.Size = New Size(96, 15)
|
||||
LabelAuthor.TabIndex = 0
|
||||
LabelAuthor.Text = "Richard Mwewa"
|
||||
'
|
||||
' LabelVersion
|
||||
'
|
||||
LabelVersion.AutoSize = True
|
||||
LabelVersion.Font = New Font("Segoe UI", 8.25F, FontStyle.Regular, GraphicsUnit.Point)
|
||||
LabelVersion.Location = New Point(80, 53)
|
||||
LabelVersion.Name = "LabelVersion"
|
||||
LabelVersion.Size = New Size(45, 13)
|
||||
LabelVersion.TabIndex = 9
|
||||
LabelVersion.Text = "Version"
|
||||
'
|
||||
' ButtonClose
|
||||
'
|
||||
ButtonClose.Location = New Point(275, 249)
|
||||
ButtonClose.Name = "ButtonClose"
|
||||
ButtonClose.Size = New Size(61, 23)
|
||||
ButtonClose.TabIndex = 6
|
||||
ButtonClose.Text = "&Close"
|
||||
ButtonClose.UseVisualStyleBackColor = True
|
||||
'
|
||||
' AboutBox
|
||||
'
|
||||
AutoScaleDimensions = New SizeF(7F, 15F)
|
||||
AutoScaleMode = AutoScaleMode.Font
|
||||
BackColor = Color.Gainsboro
|
||||
CancelButton = ButtonClose
|
||||
ClientSize = New Size(346, 285)
|
||||
Controls.Add(ButtonClose)
|
||||
Controls.Add(LabelVersion)
|
||||
Controls.Add(TabControl1)
|
||||
Controls.Add(LabelProgramName)
|
||||
Controls.Add(PictureBoxLogo)
|
||||
FormBorderStyle = FormBorderStyle.FixedSingle
|
||||
Icon = CType(resources.GetObject("$this.Icon"), Icon)
|
||||
MaximizeBox = False
|
||||
MinimizeBox = False
|
||||
Name = "AboutBox"
|
||||
ShowInTaskbar = False
|
||||
StartPosition = FormStartPosition.CenterScreen
|
||||
Text = "About"
|
||||
CType(PictureBoxLogo, ComponentModel.ISupportInitialize).EndInit()
|
||||
TabControl1.ResumeLayout(False)
|
||||
TabPageAbout.ResumeLayout(False)
|
||||
TabPageAbout.PerformLayout()
|
||||
TabPageAuthor.ResumeLayout(False)
|
||||
TabPageAuthor.PerformLayout()
|
||||
ResumeLayout(False)
|
||||
PerformLayout()
|
||||
End Sub
|
||||
|
||||
Friend WithEvents PictureBoxLogo As PictureBox
|
||||
Friend WithEvents LabelProgramName As Label
|
||||
Friend WithEvents LabelDescription As Label
|
||||
Friend WithEvents LicenseRichTextBox As RichTextBox
|
||||
Friend WithEvents DataGridView1 As DataGridView
|
||||
Friend WithEvents TabControl1 As TabControl
|
||||
Friend WithEvents TabPageAbout As TabPage
|
||||
Friend WithEvents TabPageAuthor As TabPage
|
||||
Friend WithEvents LabelVersion As Label
|
||||
Friend WithEvents LinkLabelLicense As LinkLabel
|
||||
Friend WithEvents ButtonClose As Button
|
||||
Friend WithEvents LabelCopyright As Label
|
||||
Friend WithEvents LinkLabelReadtheWiki As LinkLabel
|
||||
Friend WithEvents LabelAuthor As Label
|
||||
Friend WithEvents LinkLabelAboutMe As LinkLabel
|
||||
Friend WithEvents LinkLabelEmail As LinkLabel
|
||||
Friend WithEvents LinkLabelBMC As LinkLabel
|
||||
End Class
|
||||
@@ -1,83 +0,0 @@
|
||||
Imports System.Runtime
|
||||
|
||||
Public Class AboutBox
|
||||
ReadOnly settings As New SettingsManager()
|
||||
|
||||
''' <summary>
|
||||
''' Handles the Load event for the AboutBox form.
|
||||
''' </summary>
|
||||
''' <param name="sender">The source of the event.</param>
|
||||
''' <param name="e">The event data.</param>
|
||||
Private Sub AboutBox_Load(sender As Object, e As EventArgs) Handles MyBase.Load
|
||||
Me.Text = $"About {My.Application.Info.AssemblyName}"
|
||||
|
||||
settings.LoadSettings()
|
||||
settings.ToggleSettings(settings.DarkMode, "darkmode")
|
||||
|
||||
LabelProgramName.Text = My.Application.Info.ProductName
|
||||
LabelDescription.Text = "Retrieve Reddit posts that contain the specified keyword
|
||||
from a specified subreddit. "
|
||||
LabelVersion.Text = $"Version {My.Application.Info.Version}"
|
||||
LabelCopyright.Text = My.Application.Info.Copyright
|
||||
End Sub
|
||||
|
||||
''' <summary>
|
||||
''' Handles the LinkClicked event for the LinkLabelLicense control.
|
||||
''' Opens A MessageBox showing the License Notice.
|
||||
''' </summary>
|
||||
''' <param name="sender">The source of the event.</param>
|
||||
''' <param name="e">The event data.</param>
|
||||
Private Sub LinkLabelLicense_LinkClicked(sender As Object, e As LinkLabelLinkClickedEventArgs) Handles LinkLabelLicense.LinkClicked
|
||||
Utilities.LicenseAgreement()
|
||||
End Sub
|
||||
|
||||
''' <summary>
|
||||
''' Handles the LinkClicked event for the LinkLabelReadtheWiki control.
|
||||
''' Opens the Wiki URL in the default browser.
|
||||
''' </summary>
|
||||
''' <param name="sender">The source of the event.</param>
|
||||
''' <param name="e">The event data.</param>
|
||||
Private Sub LinkLabelReadtheWiki_LinkClicked(sender As Object, e As LinkLabelLinkClickedEventArgs) Handles LinkLabelReadtheWiki.LinkClicked
|
||||
Shell("cmd /c start https://github.com/bellingcat/reddit-post-scraping-tool/wiki")
|
||||
End Sub
|
||||
|
||||
''' <summary>
|
||||
''' Handles the LinkClicked event for the LinkLabelAboutMe control.
|
||||
''' Opens A MessageBox showing the License Notice.
|
||||
''' </summary>
|
||||
''' <param name="sender">The source of the event.</param>
|
||||
''' <param name="e">The event data.</param>
|
||||
Private Sub LinkLabelAboutMe_LinkClicked(sender As Object, e As LinkLabelLinkClickedEventArgs) Handles LinkLabelAboutMe.LinkClicked
|
||||
Shell("cmd /c start https://about.me/rly0nheart")
|
||||
End Sub
|
||||
|
||||
''' <summary>
|
||||
''' Handles the LinkClicked event for the LinkLabelBMC control.
|
||||
''' Opens A MessageBox showing the License Notice.
|
||||
''' </summary>
|
||||
''' <param name="sender">The source of the event.</param>
|
||||
''' <param name="e">The event data.</param>
|
||||
Private Sub LinkLabelBMC_LinkClicked(sender As Object, e As LinkLabelLinkClickedEventArgs) Handles LinkLabelBMC.LinkClicked
|
||||
Shell("cmd /c start https://buymeacoffee.com/_rly0nheart")
|
||||
End Sub
|
||||
|
||||
''' <summary>
|
||||
''' Handles the LinkClicked event for the LinkLabelEmail control.
|
||||
''' Opens A MessageBox showing the License Notice.
|
||||
''' </summary>
|
||||
''' <param name="sender">The source of the event.</param>
|
||||
''' <param name="e">The event data.</param>
|
||||
Private Sub LinkLabelEmail_LinkClicked(sender As Object, e As LinkLabelLinkClickedEventArgs) Handles LinkLabelEmail.LinkClicked
|
||||
Shell("cmd /c start mailto:rly0nheart@duck.com")
|
||||
End Sub
|
||||
|
||||
|
||||
''' <summary>
|
||||
''' Handles the Click event for ButtonOK event.
|
||||
''' </summary>
|
||||
''' <param name="sender">The source of the event.</param>
|
||||
''' <param name="e">The event data.</param>
|
||||
Private Sub ButtonOK_Click(sender As Object, e As EventArgs) Handles ButtonClose.Click
|
||||
Me.Close()
|
||||
End Sub
|
||||
End Class
|
||||
@@ -1,69 +0,0 @@
|
||||
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)
|
||||
''' <summary>
|
||||
''' Clear the Columns and Rows before adding Items to them.
|
||||
''' <summary>
|
||||
dataGridView.Rows.Clear()
|
||||
dataGridView.Columns.Clear()
|
||||
|
||||
dataGridView.Columns.Add("PostCount", "🔢 Index")
|
||||
dataGridView.Columns.Add("PostAuthor", "👤 Author")
|
||||
dataGridView.Columns.Add("PostID", "🆔 ID")
|
||||
dataGridView.Columns.Add("PostText", "📝 Text")
|
||||
dataGridView.Columns.Add("PostSubreddit", "🫂 Subreddit")
|
||||
dataGridView.Columns.Add("SubredditVisibility", "🫣 Visibility")
|
||||
dataGridView.Columns.Add("PostThumbnail", "🖼️ Thumbnail")
|
||||
dataGridView.Columns.Add("PostIsNSFW", "🔞 NSFW")
|
||||
dataGridView.Columns.Add("PostIsGilded", "🥇 Gilded")
|
||||
dataGridView.Columns.Add("PostUpvotes", "⬆️ Upvotes")
|
||||
dataGridView.Columns.Add("PostUpvoteRatio", "📊 Upvote Ratio")
|
||||
dataGridView.Columns.Add("PostDownvotes", "⬇️ Downvotes")
|
||||
dataGridView.Columns.Add("PostAwards", "🏆 Awards")
|
||||
dataGridView.Columns.Add("PostTopAward", "🏆 Top Award")
|
||||
dataGridView.Columns.Add("PostIsCrosspostable", "↪️ Is cross-postable?")
|
||||
dataGridView.Columns.Add("PostScore", "📈 Score")
|
||||
dataGridView.Columns.Add("PostCategory", "🟢 Category")
|
||||
dataGridView.Columns.Add("PostDomain", "🌐 Domain")
|
||||
dataGridView.Columns.Add("PostPermalink", "🔗 Permalink")
|
||||
dataGridView.Columns.Add("PostCreatedAt", "📅 Created At")
|
||||
dataGridView.Columns.Add("PostApprovedAt", "📅 Approved At")
|
||||
dataGridView.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>
|
||||
dataGridView.Rows.Add(postNumber,
|
||||
post("data")("author"),
|
||||
post("data")("id"),
|
||||
post("data")("selftext"),
|
||||
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")("category"),
|
||||
post("data")("domain"),
|
||||
post("data")("permalink"),
|
||||
post("data")("created"),
|
||||
post("data")("approved_at_utc"),
|
||||
post("data")("approved_by"))
|
||||
End Sub
|
||||
End Class
|
||||
310
RPST GUI/RPST/FormMain.Designer.vb
generated
@@ -1,310 +0,0 @@
|
||||
<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()>
|
||||
Partial Class FormMain
|
||||
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()
|
||||
components = New ComponentModel.Container()
|
||||
Dim resources As ComponentModel.ComponentResourceManager = New ComponentModel.ComponentResourceManager(GetType(FormMain))
|
||||
TextBoxKeyword = New TextBox()
|
||||
TextBoxSubreddit = New TextBox()
|
||||
ButtonSearch = New Button()
|
||||
ComboBoxTimeframe = New ComboBox()
|
||||
ComboBoxListing = New ComboBox()
|
||||
LabelKeyword = New Label()
|
||||
LabelSubreddit = New Label()
|
||||
LabelLimit = New Label()
|
||||
LabelListing = New Label()
|
||||
LabelTimeframe = New Label()
|
||||
ContextMenuStripRightClick = New ContextMenuStrip(components)
|
||||
SettingsToolStripMenuItem = New ToolStripMenuItem()
|
||||
DarkModeToolStripMenuItem = New ToolStripMenuItem()
|
||||
SavePostsToolStripMenuItem = New ToolStripMenuItem()
|
||||
ToJSONToolStripMenuItem = New ToolStripMenuItem()
|
||||
ToCSVToolStripMenuItem = New ToolStripMenuItem()
|
||||
AboutToolStripMenuItem = New ToolStripMenuItem()
|
||||
CheckForUpdatesToolStripMenuItem = New ToolStripMenuItem()
|
||||
QuitToolStripMenuItem = New ToolStripMenuItem()
|
||||
NumericUpDownLimit = New NumericUpDown()
|
||||
ToolTip = New ToolTip(components)
|
||||
ContextMenuStripRightClick.SuspendLayout()
|
||||
CType(NumericUpDownLimit, ComponentModel.ISupportInitialize).BeginInit()
|
||||
SuspendLayout()
|
||||
'
|
||||
' TextBoxKeyword
|
||||
'
|
||||
TextBoxKeyword.BackColor = SystemColors.Window
|
||||
TextBoxKeyword.ForeColor = SystemColors.WindowText
|
||||
TextBoxKeyword.Location = New Point(118, 20)
|
||||
TextBoxKeyword.Name = "TextBoxKeyword"
|
||||
TextBoxKeyword.PlaceholderText = "*Keyword"
|
||||
TextBoxKeyword.Size = New Size(100, 23)
|
||||
TextBoxKeyword.TabIndex = 0
|
||||
ToolTip.SetToolTip(TextBoxKeyword, "[required] The keyword to search for.")
|
||||
'
|
||||
' TextBoxSubreddit
|
||||
'
|
||||
TextBoxSubreddit.Location = New Point(118, 49)
|
||||
TextBoxSubreddit.Name = "TextBoxSubreddit"
|
||||
TextBoxSubreddit.PlaceholderText = "*Subreddit"
|
||||
TextBoxSubreddit.Size = New Size(100, 23)
|
||||
TextBoxSubreddit.TabIndex = 4
|
||||
ToolTip.SetToolTip(TextBoxSubreddit, "[required] The subreddit to search in.")
|
||||
'
|
||||
' ButtonSearch
|
||||
'
|
||||
ButtonSearch.Location = New Point(165, 165)
|
||||
ButtonSearch.Name = "ButtonSearch"
|
||||
ButtonSearch.Size = New Size(55, 28)
|
||||
ButtonSearch.TabIndex = 6
|
||||
ButtonSearch.Text = "Search"
|
||||
ToolTip.SetToolTip(ButtonSearch, "Hitting ENTER will also start the scraping process.")
|
||||
ButtonSearch.UseVisualStyleBackColor = True
|
||||
'
|
||||
' ComboBoxTimeframe
|
||||
'
|
||||
ComboBoxTimeframe.AutoCompleteCustomSource.AddRange(New String() {"Hour", "Day", "Week", "Month", "Year"})
|
||||
ComboBoxTimeframe.AutoCompleteSource = AutoCompleteSource.CustomSource
|
||||
ComboBoxTimeframe.FormattingEnabled = True
|
||||
ComboBoxTimeframe.Items.AddRange(New Object() {"Hour", "Day", "Week", "Month", "Year"})
|
||||
ComboBoxTimeframe.Location = New Point(118, 136)
|
||||
ComboBoxTimeframe.Name = "ComboBoxTimeframe"
|
||||
ComboBoxTimeframe.Size = New Size(100, 23)
|
||||
ComboBoxTimeframe.TabIndex = 8
|
||||
ComboBoxTimeframe.Text = "All"
|
||||
ToolTip.SetToolTip(ComboBoxTimeframe, "The time period for the posts. Default value is `All`.")
|
||||
'
|
||||
' ComboBoxListing
|
||||
'
|
||||
ComboBoxListing.AutoCompleteCustomSource.AddRange(New String() {"Controversial", "Hot", "Best", "New", "Rising"})
|
||||
ComboBoxListing.AutoCompleteSource = AutoCompleteSource.CustomSource
|
||||
ComboBoxListing.BackColor = SystemColors.Window
|
||||
ComboBoxListing.FormattingEnabled = True
|
||||
ComboBoxListing.Items.AddRange(New Object() {"Controversial", "Hot", "Best", "New", "Rising"})
|
||||
ComboBoxListing.Location = New Point(118, 107)
|
||||
ComboBoxListing.Name = "ComboBoxListing"
|
||||
ComboBoxListing.Size = New Size(100, 23)
|
||||
ComboBoxListing.TabIndex = 9
|
||||
ComboBoxListing.Text = "Top"
|
||||
ToolTip.SetToolTip(ComboBoxListing, "The type of post listings. Default value is `Top`.")
|
||||
'
|
||||
' LabelKeyword
|
||||
'
|
||||
LabelKeyword.AutoEllipsis = True
|
||||
LabelKeyword.Font = New Font("Segoe UI Semibold", 9F, FontStyle.Bold Or FontStyle.Underline, GraphicsUnit.Point)
|
||||
LabelKeyword.ForeColor = Color.Black
|
||||
LabelKeyword.Location = New Point(19, 23)
|
||||
LabelKeyword.Name = "LabelKeyword"
|
||||
LabelKeyword.Size = New Size(71, 20)
|
||||
LabelKeyword.TabIndex = 10
|
||||
LabelKeyword.Text = "Keyword:"
|
||||
'
|
||||
' LabelSubreddit
|
||||
'
|
||||
LabelSubreddit.AutoEllipsis = True
|
||||
LabelSubreddit.Font = New Font("Segoe UI Semibold", 9F, FontStyle.Bold Or FontStyle.Underline, GraphicsUnit.Point)
|
||||
LabelSubreddit.ForeColor = Color.Black
|
||||
LabelSubreddit.Location = New Point(19, 51)
|
||||
LabelSubreddit.Name = "LabelSubreddit"
|
||||
LabelSubreddit.Size = New Size(71, 23)
|
||||
LabelSubreddit.TabIndex = 11
|
||||
LabelSubreddit.Text = "Subreddit:"
|
||||
'
|
||||
' LabelLimit
|
||||
'
|
||||
LabelLimit.AutoEllipsis = True
|
||||
LabelLimit.Font = New Font("Segoe UI Semibold", 9F, FontStyle.Bold Or FontStyle.Underline, GraphicsUnit.Point)
|
||||
LabelLimit.ForeColor = Color.Black
|
||||
LabelLimit.Location = New Point(19, 80)
|
||||
LabelLimit.Name = "LabelLimit"
|
||||
LabelLimit.Size = New Size(56, 23)
|
||||
LabelLimit.TabIndex = 12
|
||||
LabelLimit.Text = "Limit:"
|
||||
'
|
||||
' LabelListing
|
||||
'
|
||||
LabelListing.AutoEllipsis = True
|
||||
LabelListing.Font = New Font("Segoe UI Semibold", 9F, FontStyle.Bold Or FontStyle.Underline, GraphicsUnit.Point)
|
||||
LabelListing.ForeColor = Color.Black
|
||||
LabelListing.Location = New Point(19, 108)
|
||||
LabelListing.Name = "LabelListing"
|
||||
LabelListing.Size = New Size(56, 23)
|
||||
LabelListing.TabIndex = 13
|
||||
LabelListing.Text = "Listing:"
|
||||
'
|
||||
' LabelTimeframe
|
||||
'
|
||||
LabelTimeframe.AutoEllipsis = True
|
||||
LabelTimeframe.Font = New Font("Segoe UI Semibold", 9F, FontStyle.Bold Or FontStyle.Underline, GraphicsUnit.Point)
|
||||
LabelTimeframe.ForeColor = Color.Black
|
||||
LabelTimeframe.Location = New Point(19, 137)
|
||||
LabelTimeframe.Name = "LabelTimeframe"
|
||||
LabelTimeframe.Size = New Size(81, 23)
|
||||
LabelTimeframe.TabIndex = 14
|
||||
LabelTimeframe.Text = "Timeframe:"
|
||||
'
|
||||
' ContextMenuStripRightClick
|
||||
'
|
||||
ContextMenuStripRightClick.Items.AddRange(New ToolStripItem() {SettingsToolStripMenuItem, AboutToolStripMenuItem, CheckForUpdatesToolStripMenuItem, QuitToolStripMenuItem})
|
||||
ContextMenuStripRightClick.Name = "ContextMenuStrip1"
|
||||
ContextMenuStripRightClick.Size = New Size(181, 114)
|
||||
'
|
||||
' SettingsToolStripMenuItem
|
||||
'
|
||||
SettingsToolStripMenuItem.DropDownItems.AddRange(New ToolStripItem() {DarkModeToolStripMenuItem, SavePostsToolStripMenuItem})
|
||||
SettingsToolStripMenuItem.Image = CType(resources.GetObject("SettingsToolStripMenuItem.Image"), Image)
|
||||
SettingsToolStripMenuItem.Name = "SettingsToolStripMenuItem"
|
||||
SettingsToolStripMenuItem.Size = New Size(180, 22)
|
||||
SettingsToolStripMenuItem.Text = "Settings"
|
||||
'
|
||||
' DarkModeToolStripMenuItem
|
||||
'
|
||||
DarkModeToolStripMenuItem.CheckOnClick = True
|
||||
DarkModeToolStripMenuItem.Image = CType(resources.GetObject("DarkModeToolStripMenuItem.Image"), Image)
|
||||
DarkModeToolStripMenuItem.Name = "DarkModeToolStripMenuItem"
|
||||
DarkModeToolStripMenuItem.Size = New Size(132, 22)
|
||||
DarkModeToolStripMenuItem.Text = "Dark Mode"
|
||||
'
|
||||
' SavePostsToolStripMenuItem
|
||||
'
|
||||
SavePostsToolStripMenuItem.AutoToolTip = True
|
||||
SavePostsToolStripMenuItem.DropDownItems.AddRange(New ToolStripItem() {ToJSONToolStripMenuItem, ToCSVToolStripMenuItem})
|
||||
SavePostsToolStripMenuItem.Image = CType(resources.GetObject("SavePostsToolStripMenuItem.Image"), Image)
|
||||
SavePostsToolStripMenuItem.Name = "SavePostsToolStripMenuItem"
|
||||
SavePostsToolStripMenuItem.Size = New Size(132, 22)
|
||||
SavePostsToolStripMenuItem.Text = "Save posts"
|
||||
'
|
||||
' ToJSONToolStripMenuItem
|
||||
'
|
||||
ToJSONToolStripMenuItem.AutoToolTip = True
|
||||
ToJSONToolStripMenuItem.CheckOnClick = True
|
||||
ToJSONToolStripMenuItem.Image = CType(resources.GetObject("ToJSONToolStripMenuItem.Image"), Image)
|
||||
ToJSONToolStripMenuItem.Name = "ToJSONToolStripMenuItem"
|
||||
ToJSONToolStripMenuItem.Size = New Size(116, 22)
|
||||
ToJSONToolStripMenuItem.Text = "to JSON"
|
||||
'
|
||||
' ToCSVToolStripMenuItem
|
||||
'
|
||||
ToCSVToolStripMenuItem.AutoToolTip = True
|
||||
ToCSVToolStripMenuItem.CheckOnClick = True
|
||||
ToCSVToolStripMenuItem.Image = CType(resources.GetObject("ToCSVToolStripMenuItem.Image"), Image)
|
||||
ToCSVToolStripMenuItem.Name = "ToCSVToolStripMenuItem"
|
||||
ToCSVToolStripMenuItem.Size = New Size(116, 22)
|
||||
ToCSVToolStripMenuItem.Text = "to CSV"
|
||||
'
|
||||
' AboutToolStripMenuItem
|
||||
'
|
||||
AboutToolStripMenuItem.AutoToolTip = True
|
||||
AboutToolStripMenuItem.Image = CType(resources.GetObject("AboutToolStripMenuItem.Image"), Image)
|
||||
AboutToolStripMenuItem.Name = "AboutToolStripMenuItem"
|
||||
AboutToolStripMenuItem.Size = New Size(180, 22)
|
||||
AboutToolStripMenuItem.Text = "About RPST"
|
||||
'
|
||||
' CheckForUpdatesToolStripMenuItem
|
||||
'
|
||||
CheckForUpdatesToolStripMenuItem.AutoToolTip = True
|
||||
CheckForUpdatesToolStripMenuItem.Image = CType(resources.GetObject("CheckForUpdatesToolStripMenuItem.Image"), Image)
|
||||
CheckForUpdatesToolStripMenuItem.Name = "CheckForUpdatesToolStripMenuItem"
|
||||
CheckForUpdatesToolStripMenuItem.Size = New Size(180, 22)
|
||||
CheckForUpdatesToolStripMenuItem.Text = "Check for Updates"
|
||||
'
|
||||
' QuitToolStripMenuItem
|
||||
'
|
||||
QuitToolStripMenuItem.AutoToolTip = True
|
||||
QuitToolStripMenuItem.Font = New Font("Segoe UI Semibold", 9F, FontStyle.Bold, GraphicsUnit.Point)
|
||||
QuitToolStripMenuItem.Image = CType(resources.GetObject("QuitToolStripMenuItem.Image"), Image)
|
||||
QuitToolStripMenuItem.Name = "QuitToolStripMenuItem"
|
||||
QuitToolStripMenuItem.Size = New Size(180, 22)
|
||||
QuitToolStripMenuItem.Text = "Quit"
|
||||
'
|
||||
' NumericUpDownLimit
|
||||
'
|
||||
NumericUpDownLimit.Location = New Point(118, 78)
|
||||
NumericUpDownLimit.Minimum = New Decimal(New Integer() {5, 0, 0, 0})
|
||||
NumericUpDownLimit.Name = "NumericUpDownLimit"
|
||||
NumericUpDownLimit.ReadOnly = True
|
||||
NumericUpDownLimit.Size = New Size(100, 23)
|
||||
NumericUpDownLimit.TabIndex = 15
|
||||
ToolTip.SetToolTip(NumericUpDownLimit, "Number of posts to go through. Default value is `10`.")
|
||||
NumericUpDownLimit.Value = New Decimal(New Integer() {10, 0, 0, 0})
|
||||
'
|
||||
' ToolTip
|
||||
'
|
||||
ToolTip.AutoPopDelay = 5000
|
||||
ToolTip.BackColor = Color.Gainsboro
|
||||
ToolTip.InitialDelay = 500
|
||||
ToolTip.ReshowDelay = 100
|
||||
ToolTip.ToolTipIcon = ToolTipIcon.Info
|
||||
ToolTip.ToolTipTitle = "Tip"
|
||||
'
|
||||
' FormMain
|
||||
'
|
||||
AutoScaleDimensions = New SizeF(7F, 15F)
|
||||
AutoScaleMode = AutoScaleMode.Font
|
||||
BackColor = SystemColors.Control
|
||||
ClientSize = New Size(239, 211)
|
||||
ContextMenuStrip = ContextMenuStripRightClick
|
||||
Controls.Add(ComboBoxTimeframe)
|
||||
Controls.Add(TextBoxKeyword)
|
||||
Controls.Add(LabelTimeframe)
|
||||
Controls.Add(LabelKeyword)
|
||||
Controls.Add(ComboBoxListing)
|
||||
Controls.Add(NumericUpDownLimit)
|
||||
Controls.Add(LabelListing)
|
||||
Controls.Add(ButtonSearch)
|
||||
Controls.Add(LabelLimit)
|
||||
Controls.Add(LabelSubreddit)
|
||||
Controls.Add(TextBoxSubreddit)
|
||||
FormBorderStyle = FormBorderStyle.FixedSingle
|
||||
Icon = CType(resources.GetObject("$this.Icon"), Icon)
|
||||
MaximizeBox = False
|
||||
Name = "FormMain"
|
||||
StartPosition = FormStartPosition.CenterScreen
|
||||
Text = "RPST"
|
||||
ContextMenuStripRightClick.ResumeLayout(False)
|
||||
CType(NumericUpDownLimit, ComponentModel.ISupportInitialize).EndInit()
|
||||
ResumeLayout(False)
|
||||
PerformLayout()
|
||||
End Sub
|
||||
|
||||
Friend WithEvents TextBoxKeyword As TextBox
|
||||
Friend WithEvents TextBoxSubreddit As TextBox
|
||||
Friend WithEvents ButtonSearch As Button
|
||||
Friend WithEvents ComboBoxTimeframe As ComboBox
|
||||
Friend WithEvents ComboBoxListing As ComboBox
|
||||
Friend WithEvents LabelKeyword As Label
|
||||
Friend WithEvents LabelSubreddit As Label
|
||||
Friend WithEvents LabelLimit As Label
|
||||
Friend WithEvents LabelListing As Label
|
||||
Friend WithEvents LabelTimeframe As Label
|
||||
Friend WithEvents ContextMenuStripRightClick As ContextMenuStrip
|
||||
Friend WithEvents SavePostsToolStripMenuItem As ToolStripMenuItem
|
||||
Friend WithEvents ToJSONToolStripMenuItem As ToolStripMenuItem
|
||||
Friend WithEvents ToCSVToolStripMenuItem As ToolStripMenuItem
|
||||
Friend WithEvents NumericUpDownLimit As NumericUpDown
|
||||
Friend WithEvents AboutToolStripMenuItem As ToolStripMenuItem
|
||||
Friend WithEvents CheckForUpdatesToolStripMenuItem As ToolStripMenuItem
|
||||
Friend WithEvents QuitToolStripMenuItem As ToolStripMenuItem
|
||||
Friend WithEvents ToolTip As ToolTip
|
||||
Friend WithEvents SettingsToolStripMenuItem As ToolStripMenuItem
|
||||
Friend WithEvents DarkModeToolStripMenuItem As ToolStripMenuItem
|
||||
Friend WithEvents SaveFoundPostsToolStripMenuItem As ToolStripMenuItem
|
||||
End Class
|
||||
@@ -1,203 +0,0 @@
|
||||
Imports Newtonsoft.Json.Linq
|
||||
|
||||
Public Class FormMain
|
||||
ReadOnly settings As New SettingsManager()
|
||||
ReadOnly ApiHandler As New ApiHandler()
|
||||
|
||||
''' <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 FormMain_Load(sender As Object, e As EventArgs) Handles MyBase.Load
|
||||
settings.LoadSettings()
|
||||
settings.ToggleSettings(enabled:=settings.DarkMode, saveTo:="darkmode")
|
||||
settings.ToggleSettings(enabled:=settings.SaveToJson, saveTo:="json")
|
||||
settings.ToggleSettings(enabled:=settings.SaveToCsv, saveTo:="csv")
|
||||
|
||||
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' box.
|
||||
''' </summary>
|
||||
''' <param name="sender">The source of the event.</param>
|
||||
''' <param name="e">An EventArgs that contains the event data.</param>
|
||||
Private Sub ToolStripMenuItemAbout_Click(sender As Object, e As EventArgs) Handles AboutToolStripMenuItem.Click
|
||||
AboutBox.Show()
|
||||
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 Async Sub ToolStripMenuItemCheckUpdates_Click(sender As Object, e As EventArgs) Handles CheckForUpdatesToolStripMenuItem.Click
|
||||
Dim data As JObject = Await ApiHandler.CheckUpdatesAsync()
|
||||
If data("tag_name").ToString = My.Application.Info.Version.ToString Then
|
||||
MessageBox.Show($"You're running the latest version v{My.Application.Info.Version} of {Me.Text}. Check again soon! :)", $"{Me.Text} v{My.Application.Info.Version}", MessageBoxButtons.OK, MessageBoxIcon.Information)
|
||||
Else
|
||||
Dim confirm As DialogResult = MessageBox.Show($"A new version v{data("tag_name")} of {Me.Text} is available, would you like to get it?
|
||||
|
||||
{data("body")}
|
||||
", $"{Me.Text} v{data("tag_name")}", MessageBoxButtons.YesNo, MessageBoxIcon.Question)
|
||||
If confirm = DialogResult.Yes Then
|
||||
Shell($"cmd /c start {data("html_url")}")
|
||||
End If
|
||||
End If
|
||||
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 ToolStripMenuItemQuit_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>
|
||||
''' Handles the click event of the ScrapeButton.
|
||||
''' Collects inputs, fetches Reddit posts based on the inputs,
|
||||
''' and processes Reddit posts.
|
||||
''' </summary>
|
||||
''' <param name="sender">The sender of the event.</param>
|
||||
''' <param name="e">The EventArgs instance containing the event data.</param>
|
||||
Private Sub ButtonScrape_Click(sender As Object, e As EventArgs) Handles ButtonSearch.Click
|
||||
settings.LoadSettings()
|
||||
PostsProcessor.ProcessRedditPosts(settings:=settings)
|
||||
End Sub
|
||||
|
||||
|
||||
''' <summary>
|
||||
''' Handles the KeyDown event for the TextBoxKeyword.
|
||||
''' Processes Reddit posts when the Enter key is pressed.
|
||||
''' </summary>
|
||||
''' <param name="sender">The source of the event.</param>
|
||||
''' <param name="e">The <see cref="KeyEventArgs"/> instance containing the event data.</param>
|
||||
Private Sub TextBoxKeyword_KeyDown(sender As Object, e As KeyEventArgs) Handles TextBoxKeyword.KeyDown
|
||||
settings.LoadSettings()
|
||||
|
||||
' Check if the Enter key is pressed
|
||||
If e.KeyCode = Keys.Enter Then
|
||||
' Prevent the beep sound that usually comes with the Enter key in a single-line TextBox
|
||||
e.SuppressKeyPress = True
|
||||
PostsProcessor.ProcessRedditPosts(settings:=settings)
|
||||
End If
|
||||
End Sub
|
||||
|
||||
|
||||
''' <summary>
|
||||
''' Handles the KeyDown event for the TextBoxSubreddit.
|
||||
''' Processes Reddit posts when the Enter key is pressed.
|
||||
''' </summary>
|
||||
''' <param name="sender">The source of the event.</param>
|
||||
''' <param name="e">The <see cref="KeyEventArgs"/> instance containing the event data.</param>
|
||||
Private Sub TextBoxSubreddit_KeyDown(sender As Object, e As KeyEventArgs) Handles TextBoxSubreddit.KeyDown
|
||||
settings.LoadSettings()
|
||||
|
||||
' Check if the Enter key is pressed
|
||||
If e.KeyCode = Keys.Enter Then
|
||||
' Prevent the beep sound that usually comes with the Enter key in a single-line TextBox
|
||||
e.SuppressKeyPress = True
|
||||
PostsProcessor.ProcessRedditPosts(settings:=settings)
|
||||
End If
|
||||
End Sub
|
||||
|
||||
|
||||
''' <summary>
|
||||
''' Handles the KeyDown event for the NumericUpDownLimit.
|
||||
''' Processes Reddit posts when the Enter key is pressed.
|
||||
''' </summary>
|
||||
''' <param name="sender">The source of the event.</param>
|
||||
''' <param name="e">The <see cref="KeyEventArgs"/> instance containing the event data.</param>
|
||||
Private Sub NumericUpDownLimit_KeyDown(sender As Object, e As KeyEventArgs) Handles NumericUpDownLimit.KeyDown
|
||||
settings.LoadSettings()
|
||||
|
||||
' Check if the Enter key is pressed
|
||||
If e.KeyCode = Keys.Enter Then
|
||||
' Prevent the beep sound that usually comes with the Enter key in a single-line TextBox
|
||||
e.SuppressKeyPress = True
|
||||
PostsProcessor.ProcessRedditPosts(settings:=settings)
|
||||
End If
|
||||
End Sub
|
||||
|
||||
|
||||
''' <summary>
|
||||
''' Handles the KeyDown event for the ComboBoxListing.
|
||||
''' Processes Reddit posts when the Enter key is pressed.
|
||||
''' </summary>
|
||||
''' <param name="sender">The source of the event.</param>
|
||||
''' <param name="e">The <see cref="KeyEventArgs"/> instance containing the event data.</param>
|
||||
Private Sub ComboBoxListing_KeyDown(sender As Object, e As KeyEventArgs) Handles ComboBoxListing.KeyDown
|
||||
settings.LoadSettings()
|
||||
|
||||
' Check if the Enter key is pressed
|
||||
If e.KeyCode = Keys.Enter Then
|
||||
' Prevent the beep sound that usually comes with the Enter key in a single-line TextBox
|
||||
e.SuppressKeyPress = True
|
||||
PostsProcessor.ProcessRedditPosts(settings:=settings)
|
||||
End If
|
||||
End Sub
|
||||
|
||||
|
||||
''' <summary>
|
||||
''' Handles the KeyDown event for the ComboBoxTimeframe.
|
||||
''' Processes Reddit posts when the Enter key is pressed.
|
||||
''' </summary>
|
||||
''' <param name="sender">The source of the event.</param>
|
||||
''' <param name="e">The <see cref="KeyEventArgs"/> instance containing the event data.</param>
|
||||
Private Sub ComboBoxTimeframe_KeyDown(sender As Object, e As KeyEventArgs) Handles ComboBoxTimeframe.KeyDown
|
||||
settings.LoadSettings()
|
||||
|
||||
' Check if the Enter key is pressed
|
||||
If e.KeyCode = Keys.Enter Then
|
||||
' Prevent the beep sound that usually comes with the Enter key in a single-line TextBox
|
||||
e.SuppressKeyPress = True
|
||||
PostsProcessor.ProcessRedditPosts(settings:=settings)
|
||||
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 ToolStripMenuItemDarkMode_CheckedChanged(sender As Object, e As EventArgs) Handles DarkModeToolStripMenuItem.CheckedChanged
|
||||
settings.ToggleSettings(enabled:=DarkModeToolStripMenuItem.Checked, saveTo:="darkmode")
|
||||
End Sub
|
||||
|
||||
''' <summary>
|
||||
''' Event handler for the 'to CSV' 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 ToCSVToolStripMenuItem_CheckedChanged(sender As Object, e As EventArgs) Handles ToCSVToolStripMenuItem.CheckedChanged
|
||||
settings.ToggleSettings(enabled:=ToCSVToolStripMenuItem.Checked, saveTo:="csv")
|
||||
End Sub
|
||||
|
||||
''' <summary>
|
||||
''' Event handler for the 'to JSON' 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 ToJSONToolStripMenuItem_CheckedChanged(sender As Object, e As EventArgs) Handles ToJSONToolStripMenuItem.CheckedChanged
|
||||
settings.ToggleSettings(enabled:=ToJSONToolStripMenuItem.Checked, saveTo:="json")
|
||||
End Sub
|
||||
End Class
|
||||
58
RPST GUI/RPST/FormPosts.Designer.vb
generated
@@ -1,58 +0,0 @@
|
||||
<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()>
|
||||
Partial Class FormPosts
|
||||
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(FormPosts))
|
||||
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(501, 365)
|
||||
DataGridViewPosts.TabIndex = 3
|
||||
'
|
||||
' FormPosts
|
||||
'
|
||||
AutoScaleDimensions = New SizeF(7F, 15F)
|
||||
AutoScaleMode = AutoScaleMode.Font
|
||||
ClientSize = New Size(501, 365)
|
||||
Controls.Add(DataGridViewPosts)
|
||||
Icon = CType(resources.GetObject("$this.Icon"), Icon)
|
||||
Name = "FormPosts"
|
||||
StartPosition = FormStartPosition.CenterScreen
|
||||
Text = "Posts"
|
||||
CType(DataGridViewPosts, ComponentModel.ISupportInitialize).EndInit()
|
||||
ResumeLayout(False)
|
||||
End Sub
|
||||
|
||||
Friend WithEvents DataGridViewPosts As DataGridView
|
||||
End Class
|
||||
@@ -1,568 +0,0 @@
|
||||
<?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:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
|
||||
<data name="$this.Icon" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
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
|
||||
8DXKzC//H2KEQtrPXsHrAAAAAElFTkSuQmCC
|
||||
</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1,5 +0,0 @@
|
||||
Public Class FormPosts
|
||||
Private Sub FormResults_Load(sender As Object, e As EventArgs) Handles MyBase.Load
|
||||
Me.Text = $"{Me.Text} - {FormMain.TextBoxKeyword.Text}, r/{FormMain.TextBoxSubreddit.Text}, {FormMain.NumericUpDownLimit.Text}, {FormMain.ComboBoxListing.Text}, {FormMain.ComboBoxTimeframe.Text}"
|
||||
End Sub
|
||||
End Class
|
||||
11
RPST GUI/RPST/My Project/Application.Designer.vb
generated
@@ -33,15 +33,10 @@ Namespace My
|
||||
|
||||
<Global.System.Diagnostics.DebuggerStepThroughAttribute()> _
|
||||
Protected Overrides Sub OnCreateMainForm()
|
||||
Me.MainForm = Global.RPST.FormMain
|
||||
Me.MainForm = Global.RPST.MainWindow
|
||||
End Sub
|
||||
|
||||
<Global.System.Diagnostics.DebuggerStepThroughAttribute()> _
|
||||
Protected Overrides Sub OnCreateSplashScreen()
|
||||
Me.SplashScreen = Global.RPST.SplashScreen
|
||||
End Sub
|
||||
|
||||
<Global.System.Diagnostics.DebuggerStepThroughAttribute()> _
|
||||
|
||||
<Global.System.Diagnostics.DebuggerStepThroughAttribute()>
|
||||
Protected Overrides Function OnInitialize(ByVal commandLineArgs As System.Collections.ObjectModel.ReadOnlyCollection(Of String)) As Boolean
|
||||
Me.MinimumSplashScreenDisplayTime = 2000
|
||||
Return MyBase.OnInitialize(commandLineArgs)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-16"?>
|
||||
<MyApplicationData xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<MySubMain>true</MySubMain>
|
||||
<MainForm>Form1</MainForm>
|
||||
<MainForm>MainWindow</MainForm>
|
||||
<SingleInstance>false</SingleInstance>
|
||||
<ShutdownMode>0</ShutdownMode>
|
||||
<EnableVisualStyles>true</EnableVisualStyles>
|
||||
<AuthenticationMode>0</AuthenticationMode>
|
||||
<SaveMySettingsOnExit>true</SaveMySettingsOnExit>
|
||||
<SplashScreen>SplashScreen</SplashScreen>
|
||||
<SplashScreen></SplashScreen>
|
||||
<MinimumSplashScreenDisplayTime>2000</MinimumSplashScreenDisplayTime>
|
||||
</MyApplicationData>
|
||||
@@ -1,88 +0,0 @@
|
||||
Imports Newtonsoft.Json.Linq
|
||||
|
||||
Public Class PostsProcessor
|
||||
Private ReadOnly ApiHandler As New ApiHandler
|
||||
|
||||
''' <summary>
|
||||
''' Asyncronously 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 Async Function FetchPostsAsync(subreddit As String, listing As String, limit As Integer, timeframe As String) As Task(Of JObject)
|
||||
Dim posts As JObject = Await ApiHandler.ScrapeRedditAsync(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(Globalization.CultureInfo.InvariantCulture).Contains(keyword.ToLower(System.Globalization.CultureInfo.InvariantCulture))
|
||||
End Function
|
||||
|
||||
|
||||
''' <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 Async Sub ProcessRedditPosts(settings)
|
||||
' Collect inputs from the user.
|
||||
Dim inputs = Utilities.CollectInputs()
|
||||
|
||||
If inputs.HasValue Then
|
||||
' Initialize the DataGridView.
|
||||
DataGridViewHandler.AddColumn(FormPosts.DataGridViewPosts)
|
||||
|
||||
' Fetch Reddit posts based on the inputs.
|
||||
Dim processor As New PostsProcessor()
|
||||
Dim posts As JObject = Await processor.FetchPostsAsync(subreddit:=inputs.Value.Subreddit, listing:=inputs.Value.Listing, limit:=inputs.Value.Limit, timeframe:=inputs.Value.Timeframe)
|
||||
Dim totalPosts As Integer = 0
|
||||
Dim keywordFound As Boolean = False
|
||||
Dim foundPosts As Integer = 0
|
||||
Dim foundPostsList As New JArray
|
||||
|
||||
' 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(Globalization.CultureInfo.InvariantCulture)) Then
|
||||
foundPosts += 1
|
||||
foundPostsList.Add(post)
|
||||
' Add the post to the DataGridView.
|
||||
DataGridViewHandler.AddRow(FormPosts.DataGridViewPosts, post, totalPosts)
|
||||
FormPosts.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(Globalization.CultureInfo.InvariantCulture) _
|
||||
+ $" {inputs.Value.Listing} posts from r/{inputs.Value.Subreddit}", "Not Found", MessageBoxButtons.OK, MessageBoxIcon.Warning)
|
||||
End If
|
||||
|
||||
|
||||
If settings.SaveToJson Then
|
||||
' Save posts to a JSON file if SaveToJson is True.
|
||||
Utilities.SavePostsToJson(posts:=foundPostsList)
|
||||
End If
|
||||
|
||||
If settings.SaveToCsv Then
|
||||
' Save posts to a CSV file if SaveToCsv is True.
|
||||
Utilities.SavePostsToCSV(posts:=foundPostsList)
|
||||
End If
|
||||
Else
|
||||
End If
|
||||
End Sub
|
||||
|
||||
End Class
|
||||
@@ -1,43 +1,52 @@
|
||||

|
||||

|
||||
|
||||
## Note
|
||||
|
||||
> Use [Knew Karma](https://github.com/bellingcat/knewkarma) for more advanced and improved features.
|
||||
|
||||
# RPST (Reddit Post Scraping Tool)
|
||||
Retrieve **Reddit** posts that contain the specified keyword from a specified subreddit.
|
||||
|
||||
Retrieve **Reddit** posts that contain the specified **keyword** from a specified **subreddit**.
|
||||
|
||||
[](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] Dark mode (*Right-click*).
|
||||
- [x] Saves results to a JSON file (*Right-click*).
|
||||
|
||||
- [x] Dark mode (*Right-click>Settings>Dark Mode*).
|
||||
- [x] Saves results to a JSON/CSV file (*Right-click>Settings>Save posts>to JSON/to CSV*).
|
||||
- [x] Logs errors to a file.
|
||||
- [x] In-App feature to check for Updates.
|
||||
|
||||
## *CLI*
|
||||
|
||||
- [x] Saves results to JSON (*specifiy* `--json`).
|
||||
- [x] Saves results to CSV (*specify* `--csv`).
|
||||
- [x] Automatically checks for new updates, and notifies user if updates 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.
|
||||
- [x] Make settings persistent in all sessions.
|
||||
- [x] Make it save results to a CSV file.
|
||||
|
||||
# 🖥️ Tested environments
|
||||
|
||||
## *GUI*
|
||||
|
||||
- [x] Microsoft Windows 11
|
||||
|
||||
## *CLI*
|
||||
|
||||
- [x] Android Termux
|
||||
- [x] Microsoft Windows 11
|
||||
- [x] Ubuntu 22.04 - latest versions
|
||||
|
||||
# 📖 Wiki
|
||||
[Refer to the Wiki](https://github.com/bellingcat/reddit-post-scraping-tool/wiki) for installation instructions, in addition to all other documentation.
|
||||
# 📖 Documentation
|
||||
|
||||
[Refer to the Wiki](https://github.com/bellingcat/reddit-post-scraping-tool/wiki) for installation instructions, in
|
||||
addition to all other documentation.
|
||||
|
||||
# 🖼️ Screenshots
|
||||
You can view a collection of screenshots for both the *CLI* and *GUI* [here](https://github.com/bellingcat/reddit-post-scraping-tool/tree/master/images)
|
||||
***
|
||||
<a href="https://www.buymeacoffee.com/_rly0nheart"><img src="https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=&slug=_rly0nheart&button_colour=40DCA5&font_colour=ffffff&font_family=Comic&outline_colour=000000&coffee_colour=FFDD00" /></a>
|
||||
|
||||

|
||||
[](https://about.me/rly0nheart)
|
||||
|
||||
@@ -6,18 +6,18 @@
|
||||
<StartupObject>RPST.My.MyApplication</StartupObject>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<MyType>WindowsForms</MyType>
|
||||
<ApplicationIcon>icon.ico</ApplicationIcon>
|
||||
<Company>Bellingcat</Company>
|
||||
<ApplicationIcon>Resources\icon.ico</ApplicationIcon>
|
||||
<Company>Richard Mwewa</Company>
|
||||
<Description>Retrieve Reddit posts that contain the specified keyword from a specified subreddit. </Description>
|
||||
<Copyright>© 2023 Richard Mwewa. All rights reserved.</Copyright>
|
||||
<PackageProjectUrl>https://github.com/bellingcat/reddit-post-scraping-tool</PackageProjectUrl>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<RepositoryUrl>https://github.com/bellingcat/reddit-post-scraping-tool</RepositoryUrl>
|
||||
<AssemblyVersion>1.9.1.1</AssemblyVersion>
|
||||
<FileVersion>1.9.1.1</FileVersion>
|
||||
<AssemblyVersion>2.0.0.0</AssemblyVersion>
|
||||
<FileVersion>2.0.0.0</FileVersion>
|
||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||
<PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>
|
||||
<Version>1.9.1</Version>
|
||||
<Version>2.0.0</Version>
|
||||
<PackageTags>reddit;scraper;reddit-scraper;osint</PackageTags>
|
||||
<PackageReleaseNotes></PackageReleaseNotes>
|
||||
<AnalysisLevel>6.0-recommended</AnalysisLevel>
|
||||
@@ -27,10 +27,11 @@
|
||||
<Product>$(AssemblyName) (Reddit Post Scraping Tool)</Product>
|
||||
<AssemblyName>RPST</AssemblyName>
|
||||
<Title>Reddit Post Scraping Tool.</Title>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="icon.ico" />
|
||||
<Content Include="Resources\icon.ico" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -77,6 +78,10 @@
|
||||
<Pack>True</Pack>
|
||||
<PackagePath>\</PackagePath>
|
||||
</None>
|
||||
<None Update="Resources\icon.png">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath>\</PackagePath>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<Compile Update="AboutBox.vb">
|
||||
<Compile Update="Windows\AboutBox.vb">
|
||||
<SubType>Form</SubType>
|
||||
</Compile>
|
||||
<Compile Update="FormMain.vb">
|
||||
<Compile Update="Windows\MainWindow.vb">
|
||||
<SubType>Form</SubType>
|
||||
</Compile>
|
||||
<Compile Update="FormPosts.vb">
|
||||
<SubType>Form</SubType>
|
||||
</Compile>
|
||||
<Compile Update="SplashScreen.vb">
|
||||
<Compile Update="Windows\PostsWindow.vb">
|
||||
<SubType>Form</SubType>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
@@ -1,222 +0,0 @@
|
||||
Imports System.IO
|
||||
Imports System.Text.Json
|
||||
Imports Newtonsoft.Json.Linq
|
||||
|
||||
|
||||
Public Class SettingsManager
|
||||
|
||||
''' <summary>
|
||||
''' Represents the Dark Mode property.
|
||||
''' Indicates whether the dark mode is enabled or disabled.
|
||||
''' </summary>
|
||||
Public Property DarkMode As Boolean
|
||||
Public Property SaveToJson As Boolean
|
||||
Public Property SaveToCsv As Boolean
|
||||
|
||||
Private ReadOnly settingsFilePath As String = Path.Combine(Environment.CurrentDirectory, "config.json")
|
||||
|
||||
''' <summary>
|
||||
''' Loads application settings from the 'settings.json' file.
|
||||
''' If the settings file doesn't exist, it creates a new file with default settings.
|
||||
''' </summary>
|
||||
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 = JsonSerializer.Deserialize(Of SettingsManager)(json, options)
|
||||
|
||||
DarkMode = settings.DarkMode
|
||||
SaveToJson = settings.SaveToJson
|
||||
SaveToCsv = settings.SaveToCsv
|
||||
|
||||
FormMain.DarkModeToolStripMenuItem.Checked = settings.DarkMode
|
||||
FormMain.ToJSONToolStripMenuItem.Checked = settings.SaveToJson
|
||||
FormMain.ToCSVToolStripMenuItem.Checked = settings.SaveToCsv
|
||||
Else
|
||||
' Settings file does not exist
|
||||
' Create a new file with default settings 'False'
|
||||
Dim defaultSettings = New SettingsManager With {.DarkMode = False, .SaveToCsv = False, .SaveToJson = False}
|
||||
Dim jsonOutput = JsonSerializer.Serialize(defaultSettings)
|
||||
File.WriteAllText(settingsFilePath, jsonOutput)
|
||||
|
||||
DarkMode = False
|
||||
SaveToJson = False
|
||||
SaveToCsv = False
|
||||
|
||||
FormMain.ToJSONToolStripMenuItem.Checked = False
|
||||
FormMain.ToCSVToolStripMenuItem.Checked = False
|
||||
FormMain.DarkModeToolStripMenuItem.Checked = False
|
||||
|
||||
|
||||
End If
|
||||
End Sub
|
||||
|
||||
|
||||
''' <summary>
|
||||
''' Retrieves application settings from a JSON file.
|
||||
''' </summary>
|
||||
''' <returns>A Dictionary containing the names and values of all settings.
|
||||
''' If the settings file doesn't exist, returns a Dictionary with default values.</returns>
|
||||
Private Function GetSettings() As Dictionary(Of String, Object)
|
||||
Dim settings As New Dictionary(Of String, Object)
|
||||
If File.Exists(settingsFilePath) Then
|
||||
' Read and parse the JSON settings file.
|
||||
Dim json As String = File.ReadAllText(settingsFilePath)
|
||||
Dim jObject As JObject = JObject.Parse(json)
|
||||
|
||||
' Loop through each property in the JObject and add it to the settings Dictionary.
|
||||
For Each item As JProperty In jObject.Properties()
|
||||
settings.Add(item.Name, item.Value.ToObject(Of Object)())
|
||||
Next
|
||||
Else
|
||||
End If
|
||||
Return settings
|
||||
End Function
|
||||
|
||||
''' <summary>
|
||||
''' Saves the provided settings to the 'settings.json' file.
|
||||
''' </summary>
|
||||
''' <param name="settings">An instance of the SettingsManager containing the configurations to be saved.</param>
|
||||
Private Sub SaveSettings(settings)
|
||||
Dim jsonOutput = JsonSerializer.Serialize(settings)
|
||||
File.WriteAllText(settingsFilePath, jsonOutput)
|
||||
End Sub
|
||||
|
||||
|
||||
''' <summary>
|
||||
''' Applies the current settings to the application's interface. This includes
|
||||
''' toggling SaveToJson, SaveToCsv, and applying the visual theme based on the Dark Mode setting.
|
||||
''' </summary>
|
||||
Public Sub ApplySettings()
|
||||
' Retrieve the current settings
|
||||
Dim settings As Dictionary(Of String, Object) = GetSettings()
|
||||
|
||||
' Apply the SaveToJson setting to the menu item checkbox
|
||||
FormMain.ToJSONToolStripMenuItem.Checked = CBool(settings("SaveToJson"))
|
||||
|
||||
' Apply the SaveToCsv setting to the menu item checkbox
|
||||
FormMain.ToCSVToolStripMenuItem.Checked = CBool(settings("SaveToCsv"))
|
||||
|
||||
' Apply the color scheme based on the Dark Mode setting
|
||||
ApplyColorScheme(isDarkMode:=CBool(settings("DarkMode")))
|
||||
End Sub
|
||||
|
||||
''' <summary>
|
||||
''' Applies the color scheme based on the given Dark Mode setting.
|
||||
''' Colors are defined in a mapping for easier maintenance and flexibility.
|
||||
''' </summary>
|
||||
''' <param name="isDarkMode">Indicates whether Dark Mode is enabled.</param>
|
||||
Public Shared Sub ApplyColorScheme(ByVal isDarkMode As Boolean)
|
||||
' Initialize color mapping
|
||||
Dim colorMap As New Dictionary(Of String, Color)
|
||||
|
||||
If isDarkMode Then
|
||||
' Dark Mode colors
|
||||
colorMap("MainBackground") = ColorTranslator.FromHtml("#FF121212")
|
||||
colorMap("TextBoxBackground") = ColorTranslator.FromHtml("#FF2E2E2E")
|
||||
colorMap("Foreground") = SystemColors.Control
|
||||
colorMap("MenuBackground") = ColorTranslator.FromHtml("#FF121212")
|
||||
colorMap("AboutBackground") = ColorTranslator.FromHtml("#FF121212")
|
||||
colorMap("AboutForeground") = SystemColors.Control
|
||||
colorMap("TabPageBackground") = ColorTranslator.FromHtml("#FF2E2E2E")
|
||||
colorMap("TabPageForeground") = SystemColors.Control
|
||||
colorMap("ButtonForeground") = Color.Black
|
||||
Else
|
||||
' Light Mode colors
|
||||
colorMap("MainBackground") = Color.Gainsboro
|
||||
colorMap("TextBoxBackground") = SystemColors.Control
|
||||
colorMap("Foreground") = ColorTranslator.FromHtml("#FF121212")
|
||||
colorMap("MenuBackground") = Color.Gainsboro
|
||||
colorMap("AboutBackground") = Color.Gainsboro
|
||||
colorMap("AboutForeground") = SystemColors.WindowText
|
||||
colorMap("TabPageBackground") = SystemColors.Control
|
||||
colorMap("TabPageForeground") = SystemColors.WindowText
|
||||
colorMap("ButtonForeground") = Color.Black
|
||||
End If
|
||||
|
||||
' Applying Main Form colors
|
||||
FormMain.BackColor = colorMap("MainBackground")
|
||||
FormMain.TextBoxKeyword.BackColor = colorMap("TextBoxBackground")
|
||||
FormMain.TextBoxSubreddit.BackColor = colorMap("TextBoxBackground")
|
||||
FormMain.NumericUpDownLimit.BackColor = colorMap("TextBoxBackground")
|
||||
FormMain.ComboBoxListing.BackColor = colorMap("TextBoxBackground")
|
||||
FormMain.ComboBoxTimeframe.BackColor = colorMap("TextBoxBackground")
|
||||
FormMain.TextBoxKeyword.ForeColor = colorMap("Foreground")
|
||||
FormMain.TextBoxSubreddit.ForeColor = colorMap("Foreground")
|
||||
FormMain.NumericUpDownLimit.ForeColor = colorMap("Foreground")
|
||||
FormMain.ComboBoxListing.ForeColor = colorMap("Foreground")
|
||||
FormMain.ComboBoxTimeframe.ForeColor = colorMap("Foreground")
|
||||
FormMain.LabelKeyword.ForeColor = colorMap("Foreground")
|
||||
FormMain.LabelSubreddit.ForeColor = colorMap("Foreground")
|
||||
FormMain.LabelLimit.ForeColor = colorMap("Foreground")
|
||||
FormMain.LabelListing.ForeColor = colorMap("Foreground")
|
||||
FormMain.LabelTimeframe.ForeColor = colorMap("Foreground")
|
||||
|
||||
' Applying Right-Click Menu colors
|
||||
FormMain.SettingsToolStripMenuItem.BackColor = colorMap("MenuBackground")
|
||||
FormMain.DarkModeToolStripMenuItem.BackColor = colorMap("MenuBackground")
|
||||
FormMain.SavePostsToolStripMenuItem.BackColor = colorMap("MenuBackground")
|
||||
FormMain.ToJSONToolStripMenuItem.BackColor = colorMap("MenuBackground")
|
||||
FormMain.ToCSVToolStripMenuItem.BackColor = colorMap("MenuBackground")
|
||||
FormMain.AboutToolStripMenuItem.BackColor = colorMap("MenuBackground")
|
||||
FormMain.CheckForUpdatesToolStripMenuItem.BackColor = colorMap("MenuBackground")
|
||||
FormMain.QuitToolStripMenuItem.BackColor = colorMap("MenuBackground")
|
||||
FormMain.SettingsToolStripMenuItem.ForeColor = colorMap("Foreground")
|
||||
FormMain.DarkModeToolStripMenuItem.ForeColor = colorMap("Foreground")
|
||||
FormMain.SavePostsToolStripMenuItem.ForeColor = colorMap("Foreground")
|
||||
FormMain.ToJSONToolStripMenuItem.ForeColor = colorMap("Foreground")
|
||||
FormMain.ToCSVToolStripMenuItem.ForeColor = colorMap("Foreground")
|
||||
FormMain.AboutToolStripMenuItem.ForeColor = colorMap("Foreground")
|
||||
FormMain.CheckForUpdatesToolStripMenuItem.ForeColor = colorMap("Foreground")
|
||||
FormMain.QuitToolStripMenuItem.ForeColor = colorMap("Foreground")
|
||||
|
||||
' Applying About Box colors
|
||||
AboutBox.BackColor = colorMap("AboutBackground")
|
||||
AboutBox.TabPageAbout.BackColor = colorMap("TabPageBackground")
|
||||
AboutBox.TabPageAuthor.BackColor = colorMap("TabPageBackground")
|
||||
AboutBox.ForeColor = colorMap("AboutForeground")
|
||||
AboutBox.LabelProgramName.ForeColor = colorMap("AboutForeground")
|
||||
AboutBox.LabelDescription.ForeColor = colorMap("AboutForeground")
|
||||
AboutBox.LabelCopyright.ForeColor = colorMap("AboutForeground")
|
||||
AboutBox.LabelVersion.ForeColor = colorMap("AboutForeground")
|
||||
AboutBox.LabelAuthor.ForeColor = colorMap("AboutForeground")
|
||||
AboutBox.ButtonClose.ForeColor = colorMap("ButtonForeground")
|
||||
|
||||
' Updating Dark Mode Text
|
||||
If isDarkMode Then
|
||||
FormMain.DarkModeToolStripMenuItem.Text = "Dark Mode: Enabled"
|
||||
Else
|
||||
FormMain.DarkModeToolStripMenuItem.Text = "Dark Mode: Disabled"
|
||||
End If
|
||||
End Sub
|
||||
|
||||
''' <summary>
|
||||
''' Toggles specific settings on or off based on the provided parameters.
|
||||
''' </summary>
|
||||
''' <param name="enabled">A Boolean indicating if the setting option should be enabled or not.</param>
|
||||
''' <param name="saveTo">A String specifying the type of setting to toggle ('json', 'csv', or 'darkmode').</param>
|
||||
Public Sub ToggleSettings(enabled As Boolean, saveTo As String)
|
||||
' Read the existing settings from the settings file
|
||||
Dim json As String = File.ReadAllText(settingsFilePath)
|
||||
Dim options As New JsonSerializerOptions With {.PropertyNameCaseInsensitive = True}
|
||||
Dim settings As SettingsManager = JsonSerializer.Deserialize(Of SettingsManager)(json, options)
|
||||
|
||||
' Update the settings based on the specified saveTo parameter
|
||||
If saveTo.ToLower(Globalization.CultureInfo.InvariantCulture) = "json" Then
|
||||
settings.SaveToJson = enabled
|
||||
ElseIf saveTo.ToLower(Globalization.CultureInfo.InvariantCulture) = "csv" Then
|
||||
settings.SaveToCsv = enabled
|
||||
ElseIf saveTo.ToLower(Globalization.CultureInfo.InvariantCulture) = "darkmode" Then
|
||||
settings.DarkMode = enabled
|
||||
Else
|
||||
' Handle unexpected value of saveTo (if needed)
|
||||
End If
|
||||
|
||||
' Save the updated settings back to the settings file
|
||||
SaveSettings(settings)
|
||||
' Apply the updated settings to the application
|
||||
ApplySettings()
|
||||
End Sub
|
||||
End Class
|
||||
127
RPST GUI/RPST/SplashScreen.Designer.vb
generated
@@ -1,127 +0,0 @@
|
||||
<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()> _
|
||||
Partial Class SplashScreen
|
||||
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
|
||||
Friend WithEvents ApplicationTitle As Label
|
||||
Friend WithEvents Version As Label
|
||||
Friend WithEvents Copyright As Label
|
||||
Friend WithEvents MainLayoutPanel As TableLayoutPanel
|
||||
Friend WithEvents DetailsLayoutPanel As TableLayoutPanel
|
||||
|
||||
'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(SplashScreen))
|
||||
MainLayoutPanel = New TableLayoutPanel()
|
||||
DetailsLayoutPanel = New TableLayoutPanel()
|
||||
Copyright = New Label()
|
||||
Version = New Label()
|
||||
ApplicationTitle = New Label()
|
||||
MainLayoutPanel.SuspendLayout()
|
||||
DetailsLayoutPanel.SuspendLayout()
|
||||
SuspendLayout()
|
||||
'
|
||||
' MainLayoutPanel
|
||||
'
|
||||
MainLayoutPanel.BackColor = Color.White
|
||||
MainLayoutPanel.BackgroundImage = CType(resources.GetObject("MainLayoutPanel.BackgroundImage"), Image)
|
||||
MainLayoutPanel.BackgroundImageLayout = ImageLayout.Stretch
|
||||
MainLayoutPanel.ColumnCount = 2
|
||||
MainLayoutPanel.ColumnStyles.Add(New ColumnStyle(SizeType.Absolute, 270F))
|
||||
MainLayoutPanel.ColumnStyles.Add(New ColumnStyle(SizeType.Absolute, 73F))
|
||||
MainLayoutPanel.Controls.Add(DetailsLayoutPanel, 1, 1)
|
||||
MainLayoutPanel.Controls.Add(ApplicationTitle, 1, 0)
|
||||
MainLayoutPanel.Dock = DockStyle.Fill
|
||||
MainLayoutPanel.Location = New Point(0, 0)
|
||||
MainLayoutPanel.Name = "MainLayoutPanel"
|
||||
MainLayoutPanel.RowStyles.Add(New RowStyle(SizeType.Absolute, 223F))
|
||||
MainLayoutPanel.RowStyles.Add(New RowStyle(SizeType.Absolute, 33F))
|
||||
MainLayoutPanel.Size = New Size(483, 313)
|
||||
MainLayoutPanel.TabIndex = 0
|
||||
'
|
||||
' DetailsLayoutPanel
|
||||
'
|
||||
DetailsLayoutPanel.Anchor = AnchorStyles.None
|
||||
DetailsLayoutPanel.BackColor = Color.Transparent
|
||||
DetailsLayoutPanel.ColumnCount = 1
|
||||
DetailsLayoutPanel.ColumnStyles.Add(New ColumnStyle(SizeType.Absolute, 142F))
|
||||
DetailsLayoutPanel.ColumnStyles.Add(New ColumnStyle(SizeType.Absolute, 142F))
|
||||
DetailsLayoutPanel.Controls.Add(Copyright, 0, 1)
|
||||
DetailsLayoutPanel.Controls.Add(Version, 0, 0)
|
||||
DetailsLayoutPanel.Location = New Point(273, 226)
|
||||
DetailsLayoutPanel.Name = "DetailsLayoutPanel"
|
||||
DetailsLayoutPanel.RowCount = 2
|
||||
DetailsLayoutPanel.RowStyles.Add(New RowStyle(SizeType.Percent, 61.70213F))
|
||||
DetailsLayoutPanel.RowStyles.Add(New RowStyle(SizeType.Percent, 38.29787F))
|
||||
DetailsLayoutPanel.Size = New Size(207, 84)
|
||||
DetailsLayoutPanel.TabIndex = 1
|
||||
'
|
||||
' Copyright
|
||||
'
|
||||
Copyright.Anchor = AnchorStyles.None
|
||||
Copyright.BackColor = Color.Transparent
|
||||
Copyright.Font = New Font("Segoe UI", 8.25F, FontStyle.Regular, GraphicsUnit.Point)
|
||||
Copyright.Location = New Point(3, 51)
|
||||
Copyright.Name = "Copyright"
|
||||
Copyright.Size = New Size(201, 33)
|
||||
Copyright.TabIndex = 2
|
||||
Copyright.Text = "Copyright"
|
||||
'
|
||||
' Version
|
||||
'
|
||||
Version.Anchor = AnchorStyles.None
|
||||
Version.BackColor = Color.Transparent
|
||||
Version.Font = New Font("Segoe UI Semibold", 9F, FontStyle.Bold Or FontStyle.Underline, GraphicsUnit.Point)
|
||||
Version.Location = New Point(3, 4)
|
||||
Version.Name = "Version"
|
||||
Version.Size = New Size(201, 43)
|
||||
Version.TabIndex = 1
|
||||
Version.Text = "Version"
|
||||
'
|
||||
' ApplicationTitle
|
||||
'
|
||||
ApplicationTitle.Anchor = AnchorStyles.None
|
||||
ApplicationTitle.BackColor = Color.Transparent
|
||||
ApplicationTitle.Font = New Font("Segoe UI", 18F, FontStyle.Bold, GraphicsUnit.Point)
|
||||
ApplicationTitle.Location = New Point(273, 0)
|
||||
ApplicationTitle.Name = "ApplicationTitle"
|
||||
ApplicationTitle.Size = New Size(207, 223)
|
||||
ApplicationTitle.TabIndex = 0
|
||||
ApplicationTitle.Text = "Reddit Post Scraping Tool."
|
||||
ApplicationTitle.TextAlign = ContentAlignment.BottomLeft
|
||||
'
|
||||
' SplashScreen
|
||||
'
|
||||
AutoScaleDimensions = New SizeF(7F, 15F)
|
||||
AutoScaleMode = AutoScaleMode.Font
|
||||
ClientSize = New Size(483, 313)
|
||||
ControlBox = False
|
||||
Controls.Add(MainLayoutPanel)
|
||||
FormBorderStyle = FormBorderStyle.FixedSingle
|
||||
MaximizeBox = False
|
||||
MinimizeBox = False
|
||||
Name = "SplashScreen"
|
||||
ShowInTaskbar = False
|
||||
StartPosition = FormStartPosition.CenterScreen
|
||||
MainLayoutPanel.ResumeLayout(False)
|
||||
DetailsLayoutPanel.ResumeLayout(False)
|
||||
ResumeLayout(False)
|
||||
End Sub
|
||||
|
||||
End Class
|
||||
@@ -1,9 +0,0 @@
|
||||
Public NotInheritable Class SplashScreen
|
||||
Private Sub SplashScreen_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Load
|
||||
' Version info
|
||||
Version.Text = $"Version {My.Application.Info.Version}"
|
||||
|
||||
'Copyright info
|
||||
Copyright.Text = My.Application.Info.Copyright
|
||||
End Sub
|
||||
End Class
|
||||
@@ -1,186 +0,0 @@
|
||||
Imports System.IO
|
||||
Imports Newtonsoft.Json
|
||||
Imports Newtonsoft.Json.Linq
|
||||
|
||||
Public Class Utilities
|
||||
''' <summary>
|
||||
''' Shows the license notice in a messagebox.
|
||||
''' </summary>
|
||||
''' <returns>
|
||||
''' Result of the Dialog (Yes/No).
|
||||
''' </returns>
|
||||
Public Shared Function LicenseAgreement()
|
||||
Dim result As DialogResult = MessageBox.Show($"MIT License
|
||||
|
||||
{My.Application.Info.Copyright}
|
||||
|
||||
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.", "License Agreement", MessageBoxButtons.OKCancel, MessageBoxIcon.Information)
|
||||
|
||||
Return result
|
||||
End Function
|
||||
|
||||
''' <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 theFormMain.
|
||||
''' Subreddit (String) - Subreddit entered by user in theFormMain.
|
||||
''' 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 = FormMain.TextBoxKeyword.Text.Trim()
|
||||
Dim subreddit As String = FormMain.TextBoxSubreddit.Text.Trim()
|
||||
' Convert the Listing and Subreddit to lowercase using InvariantCulture.
|
||||
Dim listing As String = If(String.IsNullOrEmpty(FormMain.ComboBoxListing.Text), "top", FormMain.ComboBoxListing.Text.ToLower(Globalization.CultureInfo.InvariantCulture).Trim())
|
||||
Dim timeframe As String = If(String.IsNullOrEmpty(FormMain.ComboBoxTimeframe.Text), "all", FormMain.ComboBoxTimeframe.Text.ToLower(Globalization.CultureInfo.InvariantCulture).Trim())
|
||||
Dim limit As Integer = FormMain.NumericUpDownLimit.Value
|
||||
|
||||
' Validate inputs.
|
||||
If String.IsNullOrEmpty(keyword) AndAlso String.IsNullOrEmpty(subreddit) Then
|
||||
MessageBox.Show("Keyword and Subreddit should not be empty.", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning)
|
||||
Return Nothing
|
||||
ElseIf String.IsNullOrEmpty(keyword) Then
|
||||
MessageBox.Show("Keyword field should not be empty.", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning)
|
||||
Return Nothing
|
||||
ElseIf String.IsNullOrEmpty(subreddit) Then
|
||||
MessageBox.Show("Subreddit field should not be empty.", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning)
|
||||
Return Nothing
|
||||
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}", "Saved", MessageBoxButtons.OK, MessageBoxIcon.Information)
|
||||
End If
|
||||
End Sub
|
||||
|
||||
|
||||
''' <summary>
|
||||
''' Saves Reddit posts contained in a JArray to a CSV file.
|
||||
''' </summary>
|
||||
''' <param name="posts">A JArray containing the Reddit posts to be saved.</param>
|
||||
''' <remarks>
|
||||
''' This function displays a SaveFileDialog to allow the user to specify the file name and location.
|
||||
''' It then iterates through the JArray to write each post's details (totalPosts, title, subreddit, author, score) into the selected CSV file.
|
||||
''' </remarks>
|
||||
Public Shared Sub SavePostsToCSV(posts As JArray)
|
||||
Dim saveFileDialog As New SaveFileDialog With {
|
||||
.Filter = "CSV files (*.csv)|*.csv",
|
||||
.Title = "Save posts to CSV"
|
||||
}
|
||||
|
||||
If saveFileDialog.ShowDialog() = DialogResult.OK Then
|
||||
Dim fileName As String = saveFileDialog.FileName
|
||||
Using csvWriter As New StreamWriter(fileName)
|
||||
' Write the header.
|
||||
csvWriter.WriteLine("Index,Author,ID,Subreddit,Visibility,Thumbnail,NSFW,Gilded,Upvotes,Upvote Ratio,Downvotes,Award,Top Award,Is cross-postable?,Score,Category,Text,Domain,Permalink,Created At,Approved At,Approved By")
|
||||
|
||||
Dim postCount As Integer = 0
|
||||
For Each post In posts
|
||||
postCount += 1
|
||||
csvWriter.WriteLine($"{postCount},{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")("category")},{post("data")("selftext")},{post("data")("domain")},{post("data")("permalink")},{post("data")("created")},{post("data")("approved_at_utc")},{post("data")("approved_by")}")
|
||||
Next
|
||||
End Using
|
||||
|
||||
MessageBox.Show($"Posts saved to {fileName}", "Saved", MessageBoxButtons.OK, MessageBoxIcon.Information)
|
||||
End If
|
||||
End Sub
|
||||
|
||||
|
||||
''' <summary>
|
||||
''' Checks if the "launch.log" file exists in the directory: C:\Users\<username>\AppData\Roaming\RPSTl\logs.
|
||||
''' </summary>
|
||||
''' <remarks>
|
||||
''' If the file doesn't exist, it shows a MessageBox with the License Agreement Notice with buttons Yes and No.
|
||||
''' If the user clicks on the Yes button, it creates one the launch.log file, otherwise assume the user did not agree to the License and close the program.
|
||||
''' The launc.log file is used to determine whether the program has been run before.
|
||||
''' </remarks>
|
||||
Public Shared Sub LogFirstTimeLaunch()
|
||||
Dim filePath As String = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RPST", "logs", "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
|
||||
Dim result As DialogResult = LicenseAgreement()
|
||||
If result = DialogResult.OK Then
|
||||
File.WriteAllText(filePath, textToWrite)
|
||||
Else
|
||||
FormMain.Close()
|
||||
End If
|
||||
End If
|
||||
End Sub
|
||||
End Class
|
||||
|
Before Width: | Height: | Size: 26 KiB |
792
RPST GUI/RPSTSetup/RPSTSetup.vdproj
Normal file
@@ -0,0 +1,792 @@
|
||||
"DeployProject"
|
||||
{
|
||||
"VSVersion" = "3:800"
|
||||
"ProjectType" = "8:{978C614F-708E-4E1A-B201-565925725DBA}"
|
||||
"IsWebType" = "8:FALSE"
|
||||
"ProjectName" = "8:RPSTSetup"
|
||||
"LanguageId" = "3:0"
|
||||
"CodePage" = "3:1252"
|
||||
"UILanguageId" = "3:0"
|
||||
"SccProjectName" = "8:"
|
||||
"SccLocalPath" = "8:"
|
||||
"SccAuxPath" = "8:"
|
||||
"SccProvider" = "8:"
|
||||
"Hierarchy"
|
||||
{
|
||||
"Entry"
|
||||
{
|
||||
"MsmKey" = "8:_3E2A5BCE69FF40C19B380CE7DE18F582"
|
||||
"OwnerKey" = "8:_UNDEFINED"
|
||||
"MsmSig" = "8:_UNDEFINED"
|
||||
}
|
||||
"Entry"
|
||||
{
|
||||
"MsmKey" = "8:_68E75ECCCEB74C9DAEE029419E7ACA2B"
|
||||
"OwnerKey" = "8:_UNDEFINED"
|
||||
"MsmSig" = "8:_UNDEFINED"
|
||||
}
|
||||
"Entry"
|
||||
{
|
||||
"MsmKey" = "8:_76F301ADC81F41699FE5F6EFEFECAA11"
|
||||
"OwnerKey" = "8:_UNDEFINED"
|
||||
"MsmSig" = "8:_UNDEFINED"
|
||||
}
|
||||
}
|
||||
"Configurations"
|
||||
{
|
||||
"Debug"
|
||||
{
|
||||
"DisplayName" = "8:Debug"
|
||||
"IsDebugOnly" = "11:TRUE"
|
||||
"IsReleaseOnly" = "11:FALSE"
|
||||
"OutputFilename" = "8:Debug\\RPSTSetup.msi"
|
||||
"PackageFilesAs" = "3:2"
|
||||
"PackageFileSize" = "3:-2147483648"
|
||||
"CabType" = "3:1"
|
||||
"Compression" = "3:2"
|
||||
"SignOutput" = "11:FALSE"
|
||||
"CertificateFile" = "8:"
|
||||
"PrivateKeyFile" = "8:"
|
||||
"TimeStampServer" = "8:"
|
||||
"InstallerBootstrapper" = "3:2"
|
||||
"BootstrapperCfg:{63ACBE69-63AA-4F98-B2B6-99F9E24495F2}"
|
||||
{
|
||||
"Enabled" = "11:TRUE"
|
||||
"PromptEnabled" = "11:TRUE"
|
||||
"PrerequisitesLocation" = "2:1"
|
||||
"Url" = "8:"
|
||||
"ComponentsUrl" = "8:"
|
||||
"Items"
|
||||
{
|
||||
"{EDC2488A-8267-493A-A98E-7D9C3B36CDF3}:Microsoft.NetCore.CoreRuntime.6.0.x64"
|
||||
{
|
||||
"Name" = "8:.NET Runtime 6.0.25 (x64)"
|
||||
"ProductCode" = "8:Microsoft.NetCore.CoreRuntime.6.0.x64"
|
||||
}
|
||||
"{EDC2488A-8267-493A-A98E-7D9C3B36CDF3}:Microsoft.NetCore.CoreRuntime.6.0.x86"
|
||||
{
|
||||
"Name" = "8:.NET Runtime 6.0.25 (x86)"
|
||||
"ProductCode" = "8:Microsoft.NetCore.CoreRuntime.6.0.x86"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"Release"
|
||||
{
|
||||
"DisplayName" = "8:Release"
|
||||
"IsDebugOnly" = "11:FALSE"
|
||||
"IsReleaseOnly" = "11:TRUE"
|
||||
"OutputFilename" = "8:Release\\RPSTSetup.msi"
|
||||
"PackageFilesAs" = "3:2"
|
||||
"PackageFileSize" = "3:-2147483648"
|
||||
"CabType" = "3:1"
|
||||
"Compression" = "3:2"
|
||||
"SignOutput" = "11:FALSE"
|
||||
"CertificateFile" = "8:"
|
||||
"PrivateKeyFile" = "8:"
|
||||
"TimeStampServer" = "8:"
|
||||
"InstallerBootstrapper" = "3:2"
|
||||
}
|
||||
}
|
||||
"Deployable"
|
||||
{
|
||||
"CustomAction"
|
||||
{
|
||||
}
|
||||
"DefaultFeature"
|
||||
{
|
||||
"Name" = "8:DefaultFeature"
|
||||
"Title" = "8:"
|
||||
"Description" = "8:"
|
||||
}
|
||||
"ExternalPersistence"
|
||||
{
|
||||
"LaunchCondition"
|
||||
{
|
||||
"{A06ECF26-33A3-4562-8140-9B0E340D4F24}:_17213E668C074AD5AD5FC8E06206E69E"
|
||||
{
|
||||
"Name" = "8:.NET Core"
|
||||
"Message" = "8:[VSDNETCOREMSG]"
|
||||
"AllowLaterVersions" = "11:FALSE"
|
||||
"InstallUrl" = "8:https://dotnet.microsoft.com/download/dotnet-core/[NetCoreVerMajorDotMinor]"
|
||||
"IsNETCore" = "11:TRUE"
|
||||
"Architecture" = "2:0"
|
||||
"Runtime" = "2:0"
|
||||
}
|
||||
}
|
||||
}
|
||||
"File"
|
||||
{
|
||||
"{1FB2D0AE-D3B9-43D4-B9DD-F88EC61E35DE}:_3E2A5BCE69FF40C19B380CE7DE18F582"
|
||||
{
|
||||
"SourcePath" = "8:..\\RPST\\Resources\\icon-small.ico"
|
||||
"TargetName" = "8:icon-small.ico"
|
||||
"Tag" = "8:"
|
||||
"Folder" = "8:_362735941ABB4269A087A0EAC1F3EB41"
|
||||
"Condition" = "8:"
|
||||
"Transitive" = "11:FALSE"
|
||||
"Vital" = "11:TRUE"
|
||||
"ReadOnly" = "11:TRUE"
|
||||
"Hidden" = "11:TRUE"
|
||||
"System" = "11:FALSE"
|
||||
"Permanent" = "11:FALSE"
|
||||
"SharedLegacy" = "11:FALSE"
|
||||
"PackageAs" = "3:1"
|
||||
"Register" = "3:1"
|
||||
"Exclude" = "11:FALSE"
|
||||
"IsDependency" = "11:FALSE"
|
||||
"IsolateTo" = "8:"
|
||||
}
|
||||
"{1FB2D0AE-D3B9-43D4-B9DD-F88EC61E35DE}:_76F301ADC81F41699FE5F6EFEFECAA11"
|
||||
{
|
||||
"SourcePath" = "8:..\\RPST\\LICENSE.rtf"
|
||||
"TargetName" = "8:LICENSE.rtf"
|
||||
"Tag" = "8:"
|
||||
"Folder" = "8:_362735941ABB4269A087A0EAC1F3EB41"
|
||||
"Condition" = "8:"
|
||||
"Transitive" = "11:FALSE"
|
||||
"Vital" = "11:TRUE"
|
||||
"ReadOnly" = "11:TRUE"
|
||||
"Hidden" = "11:TRUE"
|
||||
"System" = "11:FALSE"
|
||||
"Permanent" = "11:FALSE"
|
||||
"SharedLegacy" = "11:FALSE"
|
||||
"PackageAs" = "3:1"
|
||||
"Register" = "3:1"
|
||||
"Exclude" = "11:FALSE"
|
||||
"IsDependency" = "11:FALSE"
|
||||
"IsolateTo" = "8:"
|
||||
}
|
||||
}
|
||||
"FileType"
|
||||
{
|
||||
}
|
||||
"Folder"
|
||||
{
|
||||
"{1525181F-901A-416C-8A58-119130FE478E}:_14AD380FC0AA495FB87400E478966DD2"
|
||||
{
|
||||
"Name" = "8:#1919"
|
||||
"AlwaysCreate" = "11:TRUE"
|
||||
"Condition" = "8:"
|
||||
"Transitive" = "11:FALSE"
|
||||
"Property" = "8:ProgramMenuFolder"
|
||||
"Folders"
|
||||
{
|
||||
}
|
||||
}
|
||||
"{3C67513D-01DD-4637-8A68-80971EB9504F}:_362735941ABB4269A087A0EAC1F3EB41"
|
||||
{
|
||||
"DefaultLocation" = "8:[ProgramFilesFolder][Manufacturer]\\[ProductName]"
|
||||
"Name" = "8:#1925"
|
||||
"AlwaysCreate" = "11:FALSE"
|
||||
"Condition" = "8:"
|
||||
"Transitive" = "11:FALSE"
|
||||
"Property" = "8:TARGETDIR"
|
||||
"Folders"
|
||||
{
|
||||
}
|
||||
}
|
||||
"{1525181F-901A-416C-8A58-119130FE478E}:_BD020B7E2C9F475083F2EE7493C6CA56"
|
||||
{
|
||||
"Name" = "8:#1916"
|
||||
"AlwaysCreate" = "11:TRUE"
|
||||
"Condition" = "8:"
|
||||
"Transitive" = "11:FALSE"
|
||||
"Property" = "8:DesktopFolder"
|
||||
"Folders"
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
"LaunchCondition"
|
||||
{
|
||||
}
|
||||
"Locator"
|
||||
{
|
||||
}
|
||||
"MsiBootstrapper"
|
||||
{
|
||||
"LangId" = "3:0"
|
||||
"RequiresElevation" = "11:FALSE"
|
||||
}
|
||||
"Product"
|
||||
{
|
||||
"Name" = "8:Microsoft Visual Studio"
|
||||
"ProductName" = "8:RPST (Reddit Post Scraping Tool)"
|
||||
"ProductCode" = "8:{76E86D3A-14D8-426B-ADB6-C22C3D444E49}"
|
||||
"PackageCode" = "8:{D33BBB6A-B9C1-4596-A299-931B9FEE6921}"
|
||||
"UpgradeCode" = "8:{05EF97C6-762C-4254-94FF-53380A799007}"
|
||||
"AspNetVersion" = "8:4.0.30319.0"
|
||||
"RestartWWWService" = "11:FALSE"
|
||||
"RemovePreviousVersions" = "11:TRUE"
|
||||
"DetectNewerInstalledVersion" = "11:TRUE"
|
||||
"InstallAllUsers" = "11:FALSE"
|
||||
"ProductVersion" = "8:2.0.0"
|
||||
"Manufacturer" = "8:Richard Mwewa"
|
||||
"ARPHELPTELEPHONE" = "8:"
|
||||
"ARPHELPLINK" = "8:https://github.com/bellingcat/reddit-post-scraping-tool/wiki"
|
||||
"Title" = "8:RPST (Reddit Post Scraping Tool)"
|
||||
"Subject" = "8:"
|
||||
"ARPCONTACT" = "8:Richard Mwewa"
|
||||
"Keywords" = "8:"
|
||||
"ARPCOMMENTS" = "8:Retrieve Reddit posts that contain the specified keyword from a specified subreddit."
|
||||
"ARPURLINFOABOUT" = "8:https://about.me/rly0nheart"
|
||||
"ARPPRODUCTICON" = "8:_3E2A5BCE69FF40C19B380CE7DE18F582"
|
||||
"ARPIconIndex" = "3:0"
|
||||
"SearchPath" = "8:"
|
||||
"UseSystemSearchPath" = "11:TRUE"
|
||||
"TargetPlatform" = "3:0"
|
||||
"PreBuildEvent" = "8:"
|
||||
"PostBuildEvent" = "8:"
|
||||
"RunPostBuildEvent" = "3:0"
|
||||
}
|
||||
"Registry"
|
||||
{
|
||||
"HKLM"
|
||||
{
|
||||
"Keys"
|
||||
{
|
||||
"{60EA8692-D2D5-43EB-80DC-7906BF13D6EF}:_49885B940CB4489B9981836DE0A26E07"
|
||||
{
|
||||
"Name" = "8:Software"
|
||||
"Condition" = "8:"
|
||||
"AlwaysCreate" = "11:FALSE"
|
||||
"DeleteAtUninstall" = "11:FALSE"
|
||||
"Transitive" = "11:FALSE"
|
||||
"Keys"
|
||||
{
|
||||
"{60EA8692-D2D5-43EB-80DC-7906BF13D6EF}:_E3E857007AC8478B9D0C9C009F6A8BAF"
|
||||
{
|
||||
"Name" = "8:[Manufacturer]"
|
||||
"Condition" = "8:"
|
||||
"AlwaysCreate" = "11:FALSE"
|
||||
"DeleteAtUninstall" = "11:FALSE"
|
||||
"Transitive" = "11:FALSE"
|
||||
"Keys"
|
||||
{
|
||||
}
|
||||
"Values"
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
"Values"
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"HKCU"
|
||||
{
|
||||
"Keys"
|
||||
{
|
||||
"{60EA8692-D2D5-43EB-80DC-7906BF13D6EF}:_17B8420A2F55464D8FD66D76E90FE530"
|
||||
{
|
||||
"Name" = "8:Software"
|
||||
"Condition" = "8:"
|
||||
"AlwaysCreate" = "11:FALSE"
|
||||
"DeleteAtUninstall" = "11:FALSE"
|
||||
"Transitive" = "11:FALSE"
|
||||
"Keys"
|
||||
{
|
||||
"{60EA8692-D2D5-43EB-80DC-7906BF13D6EF}:_247BBD924EA946C0B4F05A907A48C9D5"
|
||||
{
|
||||
"Name" = "8:[Manufacturer]"
|
||||
"Condition" = "8:"
|
||||
"AlwaysCreate" = "11:FALSE"
|
||||
"DeleteAtUninstall" = "11:FALSE"
|
||||
"Transitive" = "11:FALSE"
|
||||
"Keys"
|
||||
{
|
||||
}
|
||||
"Values"
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
"Values"
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"HKCR"
|
||||
{
|
||||
"Keys"
|
||||
{
|
||||
}
|
||||
}
|
||||
"HKU"
|
||||
{
|
||||
"Keys"
|
||||
{
|
||||
}
|
||||
}
|
||||
"HKPU"
|
||||
{
|
||||
"Keys"
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
"Sequences"
|
||||
{
|
||||
}
|
||||
"Shortcut"
|
||||
{
|
||||
"{970C0BB2-C7D0-45D7-ABFA-7EC378858BC0}:_03274DE5011840E682E398A86F5A065D"
|
||||
{
|
||||
"Name" = "8:RPST (Reddit Post Scraping Tool)"
|
||||
"Arguments" = "8:"
|
||||
"Description" = "8:Retrieve Reddit posts that contain the specified keyword from a specified subreddit."
|
||||
"ShowCmd" = "3:1"
|
||||
"IconIndex" = "3:0"
|
||||
"Transitive" = "11:FALSE"
|
||||
"Target" = "8:_68E75ECCCEB74C9DAEE029419E7ACA2B"
|
||||
"Folder" = "8:_14AD380FC0AA495FB87400E478966DD2"
|
||||
"WorkingFolder" = "8:_362735941ABB4269A087A0EAC1F3EB41"
|
||||
"Icon" = "8:_3E2A5BCE69FF40C19B380CE7DE18F582"
|
||||
"Feature" = "8:"
|
||||
}
|
||||
"{970C0BB2-C7D0-45D7-ABFA-7EC378858BC0}:_6C793AC49B71464FA40CB4D6C361BD84"
|
||||
{
|
||||
"Name" = "8:RPST (Reddit Post Scraping Tool)"
|
||||
"Arguments" = "8:"
|
||||
"Description" = "8:Retrieve Reddit posts that contain the specified keyword from a specified subreddit."
|
||||
"ShowCmd" = "3:1"
|
||||
"IconIndex" = "3:0"
|
||||
"Transitive" = "11:FALSE"
|
||||
"Target" = "8:_68E75ECCCEB74C9DAEE029419E7ACA2B"
|
||||
"Folder" = "8:_BD020B7E2C9F475083F2EE7493C6CA56"
|
||||
"WorkingFolder" = "8:_362735941ABB4269A087A0EAC1F3EB41"
|
||||
"Icon" = "8:_3E2A5BCE69FF40C19B380CE7DE18F582"
|
||||
"Feature" = "8:"
|
||||
}
|
||||
}
|
||||
"UserInterface"
|
||||
{
|
||||
"{2479F3F5-0309-486D-8047-8187E2CE5BA0}:_260F17414CD94855B1C4E94459527B0E"
|
||||
{
|
||||
"UseDynamicProperties" = "11:FALSE"
|
||||
"IsDependency" = "11:FALSE"
|
||||
"SourcePath" = "8:<VsdDialogDir>\\VsdUserInterface.wim"
|
||||
}
|
||||
"{DF760B10-853B-4699-99F2-AFF7185B4A62}:_2E6A77A8635449DCAA91FDB47AD57928"
|
||||
{
|
||||
"Name" = "8:#1900"
|
||||
"Sequence" = "3:2"
|
||||
"Attributes" = "3:1"
|
||||
"Dialogs"
|
||||
{
|
||||
"{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_318D351DD1CD404E8A4BD093773AE2F5"
|
||||
{
|
||||
"Sequence" = "3:100"
|
||||
"DisplayName" = "8:License Agreement"
|
||||
"UseDynamicProperties" = "11:TRUE"
|
||||
"IsDependency" = "11:FALSE"
|
||||
"SourcePath" = "8:<VsdDialogDir>\\VsdAdminLicenseDlg.wid"
|
||||
"Properties"
|
||||
{
|
||||
"BannerBitmap"
|
||||
{
|
||||
"Name" = "8:BannerBitmap"
|
||||
"DisplayName" = "8:#1001"
|
||||
"Description" = "8:#1101"
|
||||
"Type" = "3:8"
|
||||
"ContextData" = "8:Bitmap"
|
||||
"Attributes" = "3:4"
|
||||
"Setting" = "3:1"
|
||||
"UsePlugInResources" = "11:TRUE"
|
||||
}
|
||||
"EulaText"
|
||||
{
|
||||
"Name" = "8:EulaText"
|
||||
"DisplayName" = "8:#1008"
|
||||
"Description" = "8:#1108"
|
||||
"Type" = "3:6"
|
||||
"ContextData" = "8:"
|
||||
"Attributes" = "3:0"
|
||||
"Setting" = "3:2"
|
||||
"Value" = "8:_76F301ADC81F41699FE5F6EFEFECAA11"
|
||||
"UsePlugInResources" = "11:TRUE"
|
||||
}
|
||||
"Sunken"
|
||||
{
|
||||
"Name" = "8:Sunken"
|
||||
"DisplayName" = "8:#1007"
|
||||
"Description" = "8:#1107"
|
||||
"Type" = "3:5"
|
||||
"ContextData" = "8:4;True=4;False=0"
|
||||
"Attributes" = "3:0"
|
||||
"Setting" = "3:0"
|
||||
"Value" = "3:4"
|
||||
"DefaultValue" = "3:4"
|
||||
"UsePlugInResources" = "11:TRUE"
|
||||
}
|
||||
}
|
||||
}
|
||||
"{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_5F8575AA5ECB4174B1C60F9DBFBC9855"
|
||||
{
|
||||
"Sequence" = "3:200"
|
||||
"DisplayName" = "8:Installation Folder"
|
||||
"UseDynamicProperties" = "11:TRUE"
|
||||
"IsDependency" = "11:FALSE"
|
||||
"SourcePath" = "8:<VsdDialogDir>\\VsdAdminFolderDlg.wid"
|
||||
"Properties"
|
||||
{
|
||||
"BannerBitmap"
|
||||
{
|
||||
"Name" = "8:BannerBitmap"
|
||||
"DisplayName" = "8:#1001"
|
||||
"Description" = "8:#1101"
|
||||
"Type" = "3:8"
|
||||
"ContextData" = "8:Bitmap"
|
||||
"Attributes" = "3:4"
|
||||
"Setting" = "3:1"
|
||||
"UsePlugInResources" = "11:TRUE"
|
||||
}
|
||||
}
|
||||
}
|
||||
"{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_7D142C585EE6498DA19A25EEBEDACFF6"
|
||||
{
|
||||
"Sequence" = "3:300"
|
||||
"DisplayName" = "8:Confirm Installation"
|
||||
"UseDynamicProperties" = "11:TRUE"
|
||||
"IsDependency" = "11:FALSE"
|
||||
"SourcePath" = "8:<VsdDialogDir>\\VsdAdminConfirmDlg.wid"
|
||||
"Properties"
|
||||
{
|
||||
"BannerBitmap"
|
||||
{
|
||||
"Name" = "8:BannerBitmap"
|
||||
"DisplayName" = "8:#1001"
|
||||
"Description" = "8:#1101"
|
||||
"Type" = "3:8"
|
||||
"ContextData" = "8:Bitmap"
|
||||
"Attributes" = "3:4"
|
||||
"Setting" = "3:1"
|
||||
"UsePlugInResources" = "11:TRUE"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"{DF760B10-853B-4699-99F2-AFF7185B4A62}:_521CEA4C4D36467CAF5E9B792033EB4B"
|
||||
{
|
||||
"Name" = "8:#1901"
|
||||
"Sequence" = "3:2"
|
||||
"Attributes" = "3:2"
|
||||
"Dialogs"
|
||||
{
|
||||
"{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_80EEC0D3EB6146438998401A4D904E1A"
|
||||
{
|
||||
"Sequence" = "3:100"
|
||||
"DisplayName" = "8:Progress"
|
||||
"UseDynamicProperties" = "11:TRUE"
|
||||
"IsDependency" = "11:FALSE"
|
||||
"SourcePath" = "8:<VsdDialogDir>\\VsdAdminProgressDlg.wid"
|
||||
"Properties"
|
||||
{
|
||||
"BannerBitmap"
|
||||
{
|
||||
"Name" = "8:BannerBitmap"
|
||||
"DisplayName" = "8:#1001"
|
||||
"Description" = "8:#1101"
|
||||
"Type" = "3:8"
|
||||
"ContextData" = "8:Bitmap"
|
||||
"Attributes" = "3:4"
|
||||
"Setting" = "3:1"
|
||||
"UsePlugInResources" = "11:TRUE"
|
||||
}
|
||||
"ShowProgress"
|
||||
{
|
||||
"Name" = "8:ShowProgress"
|
||||
"DisplayName" = "8:#1009"
|
||||
"Description" = "8:#1109"
|
||||
"Type" = "3:5"
|
||||
"ContextData" = "8:1;True=1;False=0"
|
||||
"Attributes" = "3:0"
|
||||
"Setting" = "3:0"
|
||||
"Value" = "3:1"
|
||||
"DefaultValue" = "3:1"
|
||||
"UsePlugInResources" = "11:TRUE"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"{2479F3F5-0309-486D-8047-8187E2CE5BA0}:_5EFCFC798BF24EB68DC201E4505F8489"
|
||||
{
|
||||
"UseDynamicProperties" = "11:FALSE"
|
||||
"IsDependency" = "11:FALSE"
|
||||
"SourcePath" = "8:<VsdDialogDir>\\VsdBasicDialogs.wim"
|
||||
}
|
||||
"{DF760B10-853B-4699-99F2-AFF7185B4A62}:_6BFF4405824D471EA651E90715AAACDF"
|
||||
{
|
||||
"Name" = "8:#1901"
|
||||
"Sequence" = "3:1"
|
||||
"Attributes" = "3:2"
|
||||
"Dialogs"
|
||||
{
|
||||
"{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_D34EDE0B30824A01B0B266B6079C8469"
|
||||
{
|
||||
"Sequence" = "3:100"
|
||||
"DisplayName" = "8:Progress"
|
||||
"UseDynamicProperties" = "11:TRUE"
|
||||
"IsDependency" = "11:FALSE"
|
||||
"SourcePath" = "8:<VsdDialogDir>\\VsdProgressDlg.wid"
|
||||
"Properties"
|
||||
{
|
||||
"BannerBitmap"
|
||||
{
|
||||
"Name" = "8:BannerBitmap"
|
||||
"DisplayName" = "8:#1001"
|
||||
"Description" = "8:#1101"
|
||||
"Type" = "3:8"
|
||||
"ContextData" = "8:Bitmap"
|
||||
"Attributes" = "3:4"
|
||||
"Setting" = "3:1"
|
||||
"UsePlugInResources" = "11:TRUE"
|
||||
}
|
||||
"ShowProgress"
|
||||
{
|
||||
"Name" = "8:ShowProgress"
|
||||
"DisplayName" = "8:#1009"
|
||||
"Description" = "8:#1109"
|
||||
"Type" = "3:5"
|
||||
"ContextData" = "8:1;True=1;False=0"
|
||||
"Attributes" = "3:0"
|
||||
"Setting" = "3:0"
|
||||
"Value" = "3:1"
|
||||
"DefaultValue" = "3:1"
|
||||
"UsePlugInResources" = "11:TRUE"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"{DF760B10-853B-4699-99F2-AFF7185B4A62}:_D30397AD6E1547799AC7430D693FA0FC"
|
||||
{
|
||||
"Name" = "8:#1902"
|
||||
"Sequence" = "3:1"
|
||||
"Attributes" = "3:3"
|
||||
"Dialogs"
|
||||
{
|
||||
"{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_71B305AEDF384563B2D30069B0F145DF"
|
||||
{
|
||||
"Sequence" = "3:100"
|
||||
"DisplayName" = "8:Finished"
|
||||
"UseDynamicProperties" = "11:TRUE"
|
||||
"IsDependency" = "11:FALSE"
|
||||
"SourcePath" = "8:<VsdDialogDir>\\VsdFinishedDlg.wid"
|
||||
"Properties"
|
||||
{
|
||||
"BannerBitmap"
|
||||
{
|
||||
"Name" = "8:BannerBitmap"
|
||||
"DisplayName" = "8:#1001"
|
||||
"Description" = "8:#1101"
|
||||
"Type" = "3:8"
|
||||
"ContextData" = "8:Bitmap"
|
||||
"Attributes" = "3:4"
|
||||
"Setting" = "3:1"
|
||||
"UsePlugInResources" = "11:TRUE"
|
||||
}
|
||||
"UpdateText"
|
||||
{
|
||||
"Name" = "8:UpdateText"
|
||||
"DisplayName" = "8:#1058"
|
||||
"Description" = "8:#1158"
|
||||
"Type" = "3:15"
|
||||
"ContextData" = "8:"
|
||||
"Attributes" = "3:0"
|
||||
"Setting" = "3:1"
|
||||
"Value" = "8:#1258"
|
||||
"DefaultValue" = "8:#1258"
|
||||
"UsePlugInResources" = "11:TRUE"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"{DF760B10-853B-4699-99F2-AFF7185B4A62}:_EBE4314B289A4ECEB75F0B5D00905DB4"
|
||||
{
|
||||
"Name" = "8:#1900"
|
||||
"Sequence" = "3:1"
|
||||
"Attributes" = "3:1"
|
||||
"Dialogs"
|
||||
{
|
||||
"{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_5A069E8C7FE74313BA59FE93ED3B017D"
|
||||
{
|
||||
"Sequence" = "3:200"
|
||||
"DisplayName" = "8:License Agreement"
|
||||
"UseDynamicProperties" = "11:TRUE"
|
||||
"IsDependency" = "11:FALSE"
|
||||
"SourcePath" = "8:<VsdDialogDir>\\VsdLicenseDlg.wid"
|
||||
"Properties"
|
||||
{
|
||||
"BannerBitmap"
|
||||
{
|
||||
"Name" = "8:BannerBitmap"
|
||||
"DisplayName" = "8:#1001"
|
||||
"Description" = "8:#1101"
|
||||
"Type" = "3:8"
|
||||
"ContextData" = "8:Bitmap"
|
||||
"Attributes" = "3:4"
|
||||
"Setting" = "3:1"
|
||||
"UsePlugInResources" = "11:TRUE"
|
||||
}
|
||||
"EulaText"
|
||||
{
|
||||
"Name" = "8:EulaText"
|
||||
"DisplayName" = "8:#1008"
|
||||
"Description" = "8:#1108"
|
||||
"Type" = "3:6"
|
||||
"ContextData" = "8:"
|
||||
"Attributes" = "3:0"
|
||||
"Setting" = "3:2"
|
||||
"Value" = "8:_76F301ADC81F41699FE5F6EFEFECAA11"
|
||||
"UsePlugInResources" = "11:TRUE"
|
||||
}
|
||||
"Sunken"
|
||||
{
|
||||
"Name" = "8:Sunken"
|
||||
"DisplayName" = "8:#1007"
|
||||
"Description" = "8:#1107"
|
||||
"Type" = "3:5"
|
||||
"ContextData" = "8:4;True=4;False=0"
|
||||
"Attributes" = "3:0"
|
||||
"Setting" = "3:0"
|
||||
"Value" = "3:4"
|
||||
"DefaultValue" = "3:4"
|
||||
"UsePlugInResources" = "11:TRUE"
|
||||
}
|
||||
}
|
||||
}
|
||||
"{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_69D777B0C6974DD0AFA54BDF51755C81"
|
||||
{
|
||||
"Sequence" = "3:400"
|
||||
"DisplayName" = "8:Confirm Installation"
|
||||
"UseDynamicProperties" = "11:TRUE"
|
||||
"IsDependency" = "11:FALSE"
|
||||
"SourcePath" = "8:<VsdDialogDir>\\VsdConfirmDlg.wid"
|
||||
"Properties"
|
||||
{
|
||||
"BannerBitmap"
|
||||
{
|
||||
"Name" = "8:BannerBitmap"
|
||||
"DisplayName" = "8:#1001"
|
||||
"Description" = "8:#1101"
|
||||
"Type" = "3:8"
|
||||
"ContextData" = "8:Bitmap"
|
||||
"Attributes" = "3:4"
|
||||
"Setting" = "3:1"
|
||||
"UsePlugInResources" = "11:TRUE"
|
||||
}
|
||||
}
|
||||
}
|
||||
"{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_7A7EE4F003334B01BE835D840DF961DF"
|
||||
{
|
||||
"Sequence" = "3:300"
|
||||
"DisplayName" = "8:Installation Folder"
|
||||
"UseDynamicProperties" = "11:TRUE"
|
||||
"IsDependency" = "11:FALSE"
|
||||
"SourcePath" = "8:<VsdDialogDir>\\VsdFolderDlg.wid"
|
||||
"Properties"
|
||||
{
|
||||
"BannerBitmap"
|
||||
{
|
||||
"Name" = "8:BannerBitmap"
|
||||
"DisplayName" = "8:#1001"
|
||||
"Description" = "8:#1101"
|
||||
"Type" = "3:8"
|
||||
"ContextData" = "8:Bitmap"
|
||||
"Attributes" = "3:4"
|
||||
"Setting" = "3:1"
|
||||
"UsePlugInResources" = "11:TRUE"
|
||||
}
|
||||
"InstallAllUsersVisible"
|
||||
{
|
||||
"Name" = "8:InstallAllUsersVisible"
|
||||
"DisplayName" = "8:#1059"
|
||||
"Description" = "8:#1159"
|
||||
"Type" = "3:5"
|
||||
"ContextData" = "8:1;True=1;False=0"
|
||||
"Attributes" = "3:0"
|
||||
"Setting" = "3:0"
|
||||
"Value" = "3:1"
|
||||
"DefaultValue" = "3:1"
|
||||
"UsePlugInResources" = "11:TRUE"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"{DF760B10-853B-4699-99F2-AFF7185B4A62}:_FE23A7B7220447F981819CF713DF20E5"
|
||||
{
|
||||
"Name" = "8:#1902"
|
||||
"Sequence" = "3:2"
|
||||
"Attributes" = "3:3"
|
||||
"Dialogs"
|
||||
{
|
||||
"{688940B3-5CA9-4162-8DEE-2993FA9D8CBC}:_54098FCDE6914AEE8AA27CDB24E77DAF"
|
||||
{
|
||||
"Sequence" = "3:100"
|
||||
"DisplayName" = "8:Finished"
|
||||
"UseDynamicProperties" = "11:TRUE"
|
||||
"IsDependency" = "11:FALSE"
|
||||
"SourcePath" = "8:<VsdDialogDir>\\VsdAdminFinishedDlg.wid"
|
||||
"Properties"
|
||||
{
|
||||
"BannerBitmap"
|
||||
{
|
||||
"Name" = "8:BannerBitmap"
|
||||
"DisplayName" = "8:#1001"
|
||||
"Description" = "8:#1101"
|
||||
"Type" = "3:8"
|
||||
"ContextData" = "8:Bitmap"
|
||||
"Attributes" = "3:4"
|
||||
"Setting" = "3:1"
|
||||
"UsePlugInResources" = "11:TRUE"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"MergeModule"
|
||||
{
|
||||
}
|
||||
"ProjectOutput"
|
||||
{
|
||||
"{5259A561-127C-4D43-A0A1-72F10C7B3BF8}:_68E75ECCCEB74C9DAEE029419E7ACA2B"
|
||||
{
|
||||
"SourcePath" = "8:..\\RPST\\obj\\Debug\\net6.0-windows\\apphost.exe"
|
||||
"TargetName" = "8:"
|
||||
"Tag" = "8:"
|
||||
"Folder" = "8:_362735941ABB4269A087A0EAC1F3EB41"
|
||||
"Condition" = "8:"
|
||||
"Transitive" = "11:FALSE"
|
||||
"Vital" = "11:TRUE"
|
||||
"ReadOnly" = "11:TRUE"
|
||||
"Hidden" = "11:FALSE"
|
||||
"System" = "11:FALSE"
|
||||
"Permanent" = "11:FALSE"
|
||||
"SharedLegacy" = "11:FALSE"
|
||||
"PackageAs" = "3:1"
|
||||
"Register" = "3:1"
|
||||
"Exclude" = "11:FALSE"
|
||||
"IsDependency" = "11:FALSE"
|
||||
"IsolateTo" = "8:"
|
||||
"ProjectOutputGroupRegister" = "3:1"
|
||||
"OutputConfiguration" = "8:"
|
||||
"OutputGroupCanonicalName" = "8:PublishItems"
|
||||
"OutputProjectGuid" = "8:{46C2541E-6F65-461A-A479-F65D445C36EA}"
|
||||
"ShowKeyOutput" = "11:TRUE"
|
||||
"ExcludeFilters"
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 576 KiB |
|
Before Width: | Height: | Size: 519 KiB |
|
Before Width: | Height: | Size: 823 KiB |
|
Before Width: | Height: | Size: 508 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1017 KiB |
|
Before Width: | Height: | Size: 886 KiB |
@@ -7,27 +7,27 @@ packages = ["rpst"]
|
||||
|
||||
[project]
|
||||
name = "reddit-post-scraping-tool"
|
||||
version = "1.9.1.1"
|
||||
version = "2.0.0.0"
|
||||
description = "Retrieve Reddit posts that contain the specified keyword from a specified subreddit."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.8"
|
||||
license = {file = "LICENSE"}
|
||||
license = { file = "LICENSE" }
|
||||
keywords = ["reddit-crawler", "reddit-scraping", "reddit", "reddit-api"]
|
||||
authors = [{name = "Richard Mwewa", email = "rly0nheart@duck.com"}]
|
||||
authors = [{ name = "Richard Mwewa", email = "rly0nheart@duck.com" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Visual Basic",
|
||||
"Intended Audience :: End Users/Desktop",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
"Natural Language :: English"
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Visual Basic",
|
||||
"Intended Audience :: End Users/Desktop",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
"Natural Language :: English"
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
"rich",
|
||||
"glyphoji",
|
||||
"requests",
|
||||
"aiohttp",
|
||||
"rich-argparse"
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
@@ -36,4 +36,5 @@ documentation = "https://github.com/bellingcat/reddit-post-scraping-tool/wiki"
|
||||
repository = "https://github.com/bellingcat/reddit-post-scraping-tool.git"
|
||||
|
||||
[project.scripts]
|
||||
rpst = "rpst.main:run"
|
||||
rpst = "rpst.scraper:run"
|
||||
reddit_post_scraping_tool = "rpst.scraper:run"
|
||||
|
||||
@@ -1 +1,42 @@
|
||||
import os
|
||||
|
||||
__author__: str = "Richard Mwewa"
|
||||
__about_author__: str = "https://about/me/rly0nheart"
|
||||
__version__: str = "2.0.0.0"
|
||||
|
||||
__description__: str = f"""
|
||||
# RPST (Reddit Post Scraping Tool) {__version__}
|
||||
> Retrieve Reddit posts that contain the specified keyword from a specified subreddit.
|
||||
"""
|
||||
__epilog__: str = f"""
|
||||
# by [{__author__}]({__about_author__})
|
||||
|
||||
```
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 {__author__}
|
||||
|
||||
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.
|
||||
```
|
||||
"""
|
||||
|
||||
# Construct path to the program's directory
|
||||
PROGRAM_DIRECTORY: str = os.path.expanduser(
|
||||
os.path.join("~", "reddit_post_scraping_tool")
|
||||
)
|
||||
|
||||
164
rpst/api.py
Normal file
@@ -0,0 +1,164 @@
|
||||
# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ #
|
||||
|
||||
from typing import Union
|
||||
|
||||
import aiohttp
|
||||
|
||||
from .coreutils import log
|
||||
|
||||
# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ #
|
||||
|
||||
|
||||
REDDIT_ENDPOINT: str = "https://www.reddit.com"
|
||||
PYPI_PROJECT_ENDPOINT: str = "https://pypi.org/pypi/reddit-post-scraping-tool/json"
|
||||
|
||||
|
||||
# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ #
|
||||
|
||||
|
||||
async def get_data(session: aiohttp.ClientSession, endpoint: str) -> Union[dict, list]:
|
||||
"""
|
||||
Fetches JSON data from a given API endpoint.
|
||||
|
||||
:param session: aiohttp session to use for the request.
|
||||
:param endpoint: The API endpoint to fetch data from.
|
||||
:return: Returns JSON data as a dictionary or list. Returns an empty dict if fetching fails.
|
||||
"""
|
||||
|
||||
try:
|
||||
async with session.get(
|
||||
endpoint,
|
||||
) as response:
|
||||
if response.status == 200:
|
||||
return await response.json()
|
||||
else:
|
||||
error_message = await response.json()
|
||||
log.error(f"An API error occurred: {error_message}")
|
||||
return {}
|
||||
|
||||
except aiohttp.ClientConnectionError as error:
|
||||
log.error(f"An HTTP error occurred: {error}")
|
||||
return {}
|
||||
except Exception as error:
|
||||
log.critical(f"An unknown error occurred: {error}")
|
||||
return {}
|
||||
|
||||
|
||||
# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ #
|
||||
|
||||
|
||||
async def get_updates(session: aiohttp.ClientSession):
|
||||
"""
|
||||
Gets and compares the current program version with the remote version.
|
||||
|
||||
Assumes version format: major.minor.patch.prefix
|
||||
|
||||
:param session: aiohttp session to use for the request.
|
||||
"""
|
||||
from . import __version__
|
||||
|
||||
# Make a GET request to PyPI to get the project's latest release.
|
||||
response: dict = await get_data(endpoint=PYPI_PROJECT_ENDPOINT, session=session)
|
||||
|
||||
if response.get("info"):
|
||||
release: dict = response.get("info")
|
||||
remote_version: str = release.get("version")
|
||||
# Splitting the version strings into components
|
||||
remote_parts: list = remote_version.split(".")
|
||||
local_parts: list = __version__.split(".")
|
||||
|
||||
update_message: str = ""
|
||||
|
||||
# Check for differences in version parts
|
||||
if remote_parts[0] != local_parts[0]:
|
||||
update_message = (
|
||||
f"MAJOR update ({remote_version}) available."
|
||||
f" It might introduce significant changes."
|
||||
)
|
||||
|
||||
elif remote_parts[1] != local_parts[1]:
|
||||
update_message = (
|
||||
f"MINOR update ({remote_version}) available."
|
||||
f" Includes small feature changes/improvements."
|
||||
)
|
||||
|
||||
elif remote_parts[2] != local_parts[2]:
|
||||
update_message = (
|
||||
f"PATCH update ({remote_version}) available."
|
||||
f" Generally for bug fixes and small tweaks."
|
||||
)
|
||||
|
||||
elif (
|
||||
len(remote_parts) > 3
|
||||
and len(local_parts) > 3
|
||||
and remote_parts[3] != local_parts[3]
|
||||
):
|
||||
update_message = (
|
||||
f"BUILD update ({remote_version}) available."
|
||||
f" Might be for specific builds or special versions."
|
||||
)
|
||||
|
||||
if update_message:
|
||||
log.info(update_message)
|
||||
|
||||
|
||||
# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ #
|
||||
async def get_posts(
|
||||
subreddit: str,
|
||||
listing: str,
|
||||
timeframe: str,
|
||||
limit: int,
|
||||
session: aiohttp.ClientSession,
|
||||
) -> list:
|
||||
all_posts = await paginated_posts(
|
||||
posts_endpoint=f"{REDDIT_ENDPOINT}/r/{subreddit}/{listing}.json?limit={limit}&t={timeframe}",
|
||||
limit=limit,
|
||||
session=session,
|
||||
)
|
||||
|
||||
return all_posts[:limit]
|
||||
|
||||
|
||||
# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ #
|
||||
|
||||
|
||||
async def paginated_posts(
|
||||
posts_endpoint: str, limit: int, session: aiohttp.ClientSession
|
||||
) -> list:
|
||||
"""
|
||||
Paginates and retrieves posts until the specified limit is reached.
|
||||
|
||||
:param posts_endpoint: API endpoint for retrieving posts.
|
||||
:param limit: Limit of the number of posts to retrieve.
|
||||
:param session: aiohttp session to use for the request.
|
||||
:return: A list of all posts.
|
||||
"""
|
||||
all_posts: list = []
|
||||
last_post_id: str = ""
|
||||
|
||||
# Determine whether to use the 'after' parameter
|
||||
use_after: bool = limit > 100
|
||||
|
||||
while len(all_posts) < limit:
|
||||
# Make the API request with the 'after' parameter if it's provided and the limit is more than 100
|
||||
if use_after and last_post_id:
|
||||
endpoint_with_after: str = f"{posts_endpoint}&after={last_post_id}"
|
||||
else:
|
||||
endpoint_with_after: str = posts_endpoint
|
||||
|
||||
posts_data: dict = await get_data(endpoint=endpoint_with_after, session=session)
|
||||
posts_children: list = posts_data.get("data", {}).get("children", [])
|
||||
|
||||
# If there are no more posts, break out of the loop
|
||||
if not posts_children:
|
||||
break
|
||||
|
||||
all_posts.extend(posts_children)
|
||||
|
||||
# We use the id of the last post in the list to paginate to the next posts
|
||||
last_post_id: str = all_posts[-1].get("data").get("id")
|
||||
|
||||
return all_posts
|
||||
|
||||
|
||||
# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ #
|
||||
103
rpst/base.py
Normal file
@@ -0,0 +1,103 @@
|
||||
# +++++++++++++++++++++++++++++++++++++++++++++++++ #
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
|
||||
import aiohttp
|
||||
|
||||
from .api import get_posts, get_updates
|
||||
from .coreutils import timestamp_to_utc
|
||||
|
||||
|
||||
# +++++++++++++++++++++++++++++++++++++++++++++++++ #
|
||||
|
||||
|
||||
@dataclass
|
||||
class Post:
|
||||
id: str
|
||||
thumbnail: str
|
||||
title: str
|
||||
text: str
|
||||
author: str
|
||||
subreddit: str
|
||||
subreddit_id: str
|
||||
subreddit_type: str
|
||||
upvotes: int
|
||||
upvote_ratio: float
|
||||
downvotes: int
|
||||
gilded: int
|
||||
is_nsfw: bool
|
||||
is_shareable: bool
|
||||
is_edited: bool
|
||||
comments: int
|
||||
hide_from_bots: bool
|
||||
score: float
|
||||
domain: str
|
||||
permalink: str
|
||||
is_locked: bool
|
||||
is_archived: bool
|
||||
created_at: str
|
||||
raw_post: dict
|
||||
|
||||
|
||||
# +++++++++++++++++++++++++++++++++++++++++++++++++ #
|
||||
|
||||
|
||||
async def find_posts(
|
||||
keyword: str,
|
||||
subreddit: str,
|
||||
listing: str,
|
||||
timeframe: str,
|
||||
limit: int,
|
||||
) -> List[Post]:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
found_posts_count: int = 0
|
||||
found_posts_list: list = []
|
||||
|
||||
await get_updates(session=session)
|
||||
raw_posts: list = await get_posts(
|
||||
subreddit=subreddit,
|
||||
listing=listing,
|
||||
timeframe=timeframe,
|
||||
limit=limit,
|
||||
session=session,
|
||||
)
|
||||
for raw_post in raw_posts:
|
||||
post_data: dict = raw_post.get("data")
|
||||
|
||||
if keyword.lower() in post_data.get(
|
||||
"selftext"
|
||||
) or keyword.lower() in post_data.get("title"):
|
||||
found_posts_count += 1
|
||||
post = Post(
|
||||
id=post_data.get("id"),
|
||||
thumbnail=post_data.get("thumbnail"),
|
||||
title=post_data.get("title"),
|
||||
text=post_data.get("selftext"),
|
||||
author=post_data.get("author"),
|
||||
subreddit=post_data.get("subreddit"),
|
||||
subreddit_id=post_data.get("subreddit_id"),
|
||||
subreddit_type=post_data.get("subreddit_type"),
|
||||
upvotes=post_data.get("ups"),
|
||||
upvote_ratio=post_data.get("upvote_ratio"),
|
||||
downvotes=post_data.get("downs"),
|
||||
gilded=post_data.get("gilded"),
|
||||
is_nsfw=post_data.get("over_18"),
|
||||
is_shareable=post_data.get("is_reddit_media_domain"),
|
||||
is_edited=post_data.get("edited"),
|
||||
comments=post_data.get("num_comments"),
|
||||
hide_from_bots=post_data.get("is_robot_indexable"),
|
||||
score=post_data.get("score"),
|
||||
domain=post_data.get("domain"),
|
||||
permalink=post_data.get("permalink"),
|
||||
is_locked=post_data.get("locked"),
|
||||
is_archived=post_data.get("archived"),
|
||||
created_at=timestamp_to_utc(timestamp=post_data.get("created_utc")),
|
||||
raw_post=post_data,
|
||||
)
|
||||
found_posts_list.append(post)
|
||||
|
||||
return found_posts_list
|
||||
|
||||
|
||||
# +++++++++++++++++++++++++++++++++++++++++++++++++ #
|
||||
170
rpst/coreutils.py
Normal file
@@ -0,0 +1,170 @@
|
||||
# +++++++++++++++++++++++++++++++++++++++++++++++++ #
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
from rich.logging import RichHandler
|
||||
from rich.markdown import Markdown
|
||||
from rich_argparse import RichHelpFormatter
|
||||
|
||||
|
||||
# +++++++++++++++++++++++++++++++++++++++++++++++++ #
|
||||
|
||||
|
||||
def timestamp_to_utc(timestamp: int) -> str:
|
||||
"""
|
||||
Converts a Unix timestamp to a formatted datetime string.
|
||||
|
||||
:param timestamp: The Unix timestamp to be converted.
|
||||
:return: A formatted datetime string in the format "dd MMMM yyyy, hh:mm:ssAM/PM".
|
||||
"""
|
||||
utc_from_timestamp: datetime = datetime.utcfromtimestamp(timestamp)
|
||||
datetime_string: str = utc_from_timestamp.strftime("%d %B %Y, %I:%M:%S%p")
|
||||
return datetime_string
|
||||
|
||||
|
||||
# +++++++++++++++++++++++++++++++++++++++++++++++++ #
|
||||
|
||||
|
||||
def pathfinder(directories: list[str]):
|
||||
for directory in directories:
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
|
||||
|
||||
# +++++++++++++++++++++++++++++++++++++++++++++++++ #
|
||||
|
||||
|
||||
def save_posts(
|
||||
filename: str,
|
||||
save_to_dir: str,
|
||||
posts: list,
|
||||
save_json: bool = False,
|
||||
save_csv: bool = False,
|
||||
):
|
||||
posts_data: list = [post.__dict__ for post in posts]
|
||||
|
||||
if save_json:
|
||||
json_path = os.path.join(os.path.join(save_to_dir, "json"), f"{filename}.json")
|
||||
with open(json_path, "w", encoding="utf-8") as json_file:
|
||||
json.dump(posts_data, json_file, indent=4)
|
||||
log.info(
|
||||
f"{os.path.getsize(json_file.name)} bytes written to [link file://{json_file.name}]{json_file.name}"
|
||||
)
|
||||
|
||||
if save_csv:
|
||||
csv_path = os.path.join(os.path.join(save_to_dir, "csv"), f"{filename}.csv")
|
||||
with open(csv_path, "w", newline="", encoding="utf-8") as csv_file:
|
||||
writer = csv.writer(csv_file)
|
||||
if posts:
|
||||
writer.writerow(
|
||||
posts_data[0].keys()
|
||||
) # header from keys of the first item
|
||||
for post in posts:
|
||||
writer.writerow(post.__dict__.values())
|
||||
log.info(
|
||||
f"{os.path.getsize(csv_file.name)} bytes written to [link file://{csv_file.name}]{csv_file.name}"
|
||||
)
|
||||
|
||||
|
||||
# +++++++++++++++++++++++++++++++++++++++++++++++++ #
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
from . import __version__, __description__, __epilog__
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description=Markdown(__description__, style="argparse.text"),
|
||||
epilog=Markdown(__epilog__, style="argparse.text"),
|
||||
formatter_class=RichHelpFormatter,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"keyword",
|
||||
help="keyword to search for, in posts",
|
||||
)
|
||||
parser.add_argument("subreddit", help="subreddit to scrape")
|
||||
parser.add_argument(
|
||||
"-l",
|
||||
"--limit",
|
||||
help="maximum number of posts to scrape (default: %(default)s)",
|
||||
default=200,
|
||||
type=int,
|
||||
)
|
||||
parser.add_argument(
|
||||
"-ls",
|
||||
"--listing",
|
||||
default="top",
|
||||
const="top",
|
||||
nargs="?",
|
||||
choices=["best", "controversial", "hot", "new", "rising", "top"],
|
||||
help="listing 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="timeframe from which to scrape posts (default: %(default)s)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-j",
|
||||
"--json",
|
||||
help="write found posts to a json file",
|
||||
action="store_true",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--csv",
|
||||
help="write found posts to a csv file",
|
||||
action="store_true",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-d",
|
||||
"--debug",
|
||||
help="(dev) run rpst in debug mode",
|
||||
action="store_true",
|
||||
)
|
||||
parser.add_argument("-v", "--version", action="version", version=__version__)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
# +++++++++++++++++++++++++++++++++++++++++++++++++ #
|
||||
|
||||
|
||||
def set_loglevel(debug_mode: bool) -> logging.getLogger:
|
||||
"""
|
||||
Configure and return a logging object with the specified log level.
|
||||
|
||||
:param debug_mode: If True, the log level is set to "NOTSET". Otherwise, it is set to "INFO".
|
||||
:return: A logging object configured with the specified log level.
|
||||
"""
|
||||
logging.basicConfig(
|
||||
level="DEBUG" if debug_mode else "INFO",
|
||||
format="%(message)s",
|
||||
handlers=[
|
||||
RichHandler(
|
||||
markup=True, log_time_format="%I:%M:%S%p", show_level=debug_mode
|
||||
)
|
||||
],
|
||||
)
|
||||
return logging.getLogger("RPST (Reddit Post Scraping Tool)")
|
||||
|
||||
|
||||
# +++++++++++++++++++++++++++++++++++++++++++++++++ #
|
||||
|
||||
args: argparse = create_parser().parse_args()
|
||||
log: logging.getLogger = set_loglevel(debug_mode=args.debug)
|
||||
|
||||
# +++++++++++++++++++++++++++++++++++++++++++++++++ #
|
||||
33
rpst/main.py
@@ -1,33 +0,0 @@
|
||||
from datetime import datetime
|
||||
|
||||
from .rpst import get_posts
|
||||
from .utils import create_parser, set_loglevel, check_updates
|
||||
|
||||
|
||||
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()
|
||||
args = parser.parse_args()
|
||||
|
||||
log = set_loglevel(debug_mode=args.debug)
|
||||
|
||||
# Record the start time
|
||||
start_time = datetime.now()
|
||||
|
||||
try:
|
||||
# Check for updates
|
||||
check_updates(version_tag="1.9.1.1")
|
||||
|
||||
# Get posts with the provided/parsed arguments
|
||||
get_posts(args=args)
|
||||
except KeyboardInterrupt:
|
||||
log.warning("User interruption detected ([yellow]Ctrl+C[/]).")
|
||||
except Exception as e:
|
||||
log.error(f"An error occurred: [red]{e}[/]")
|
||||
finally:
|
||||
log.info(f"Finished in {datetime.now() - start_time} seconds.")
|
||||
94
rpst/scraper.py
Normal file
@@ -0,0 +1,94 @@
|
||||
# +++++++++++++++++++++++++++++++++++++++++++++++++ #
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
from rich.pretty import pprint
|
||||
|
||||
from . import __version__, PROGRAM_DIRECTORY
|
||||
from .base import find_posts
|
||||
from .coreutils import args, log, save_posts, pathfinder
|
||||
|
||||
|
||||
# +++++++++++++++++++++++++++++++++++++++++++++++++ #
|
||||
|
||||
|
||||
def run():
|
||||
"""Main entry point for rpst or rpst."""
|
||||
# ------------------------------------- #
|
||||
|
||||
keyword: str = args.keyword
|
||||
subreddit: str = args.subreddit
|
||||
listing: str = args.listing
|
||||
limit: int = args.limit
|
||||
|
||||
# ------------------------------------- #
|
||||
|
||||
start_time = datetime.now()
|
||||
|
||||
# ------------------------------------- #
|
||||
|
||||
print(
|
||||
"""
|
||||
┳┓┏┓┏┓┏┳┓
|
||||
┣┫┃┃┗┓ ┃
|
||||
┛┗┣┛┗┛ ┻ """
|
||||
)
|
||||
|
||||
# ------------------------------------- #
|
||||
|
||||
try:
|
||||
log.info(
|
||||
f"[bold]RPST[/] {__version__} started at {start_time.strftime('%a %b %d %Y, %I:%M:%S%p')}..."
|
||||
)
|
||||
|
||||
found_posts = asyncio.run(
|
||||
find_posts(
|
||||
keyword=keyword,
|
||||
subreddit=subreddit,
|
||||
listing=listing,
|
||||
timeframe=args.timeframe,
|
||||
limit=limit,
|
||||
),
|
||||
)
|
||||
|
||||
if found_posts:
|
||||
pprint(
|
||||
found_posts,
|
||||
expand_all=True,
|
||||
)
|
||||
log.info(
|
||||
f"'{subreddit}': Found {len(found_posts)}/{limit} {listing} posts containing the keyword ('{keyword}')"
|
||||
)
|
||||
if args.json or args.csv:
|
||||
target_dir: str = os.path.join(PROGRAM_DIRECTORY, subreddit)
|
||||
pathfinder(
|
||||
directories=[
|
||||
os.path.join(target_dir, "csv"),
|
||||
os.path.join(target_dir, "json"),
|
||||
]
|
||||
)
|
||||
save_posts(
|
||||
filename=keyword,
|
||||
save_to_dir=target_dir,
|
||||
posts=found_posts,
|
||||
save_json=args.json,
|
||||
save_csv=args.csv,
|
||||
)
|
||||
else:
|
||||
log.info(
|
||||
f"'r/{subreddit}': No {listing} posts found that contain the keyword ('{keyword}')"
|
||||
)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
log.warning("User interruption detected ([yellow]Ctrl+C[/])")
|
||||
except Exception as error:
|
||||
log.error(f"An error occurred: [red]{error}[/]")
|
||||
finally:
|
||||
log.info(f"Finished in {datetime.now() - start_time} seconds")
|
||||
|
||||
# ------------------------------------- #
|
||||
|
||||
|
||||
# +++++++++++++++++++++++++++++++++++++++++++++++++ #
|
||||
182
rpst/utils.py
@@ -1,182 +0,0 @@
|
||||
import os
|
||||
import csv
|
||||
import json
|
||||
import logging
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
|
||||
import requests
|
||||
from glyphoji import glyph
|
||||
from rich import print
|
||||
from rich.tree import Tree
|
||||
|
||||
from rich.markdown import Markdown
|
||||
from rich.logging import RichHandler
|
||||
|
||||
|
||||
def convert_timestamp_to_datetime(timestamp: int) -> str:
|
||||
"""
|
||||
Converts a Unix timestamp to a formatted datetime string.
|
||||
|
||||
:param timestamp: The Unix timestamp to be converted.
|
||||
:return: A formatted datetime string in the format "dd MMMM yyyy, hh:mm:ssAM/PM".
|
||||
"""
|
||||
utc_from_timestamp = datetime.utcfromtimestamp(timestamp)
|
||||
datetime_object = utc_from_timestamp.strftime("%d %B %Y, %I:%M:%S%p")
|
||||
return datetime_object
|
||||
|
||||
|
||||
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="Retrieve Reddit posts that contain the specified keyword from a specified subreddit."
|
||||
)
|
||||
|
||||
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(
|
||||
"--json",
|
||||
help="Write all found posts to a json file.",
|
||||
action="store_true",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--csv",
|
||||
help="Write all found posts to a csv file.",
|
||||
action="store_true",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-d",
|
||||
"--debug",
|
||||
help="run rpst in debug mode",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
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"]
|
||||
|
||||
# Log an info message about the new release.
|
||||
print(
|
||||
f"{glyph.up_arrow} 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.
|
||||
print(Markdown(raw_release_notes))
|
||||
|
||||
|
||||
def set_loglevel(debug_mode: bool) -> logging.getLogger:
|
||||
"""
|
||||
Configure and return a logging object with the specified log level.
|
||||
|
||||
:param debug_mode: If True, the log level is set to "NOTSET". Otherwise, it is set to "INFO".
|
||||
:return: A logging object configured with the specified log level.
|
||||
"""
|
||||
logging.basicConfig(
|
||||
level="NOTSET" if debug_mode else "INFO",
|
||||
format="%(message)s",
|
||||
handlers=[
|
||||
RichHandler(markup=True, log_time_format="[%I:%M:%S %p]", show_level=False)
|
||||
],
|
||||
)
|
||||
return logging.getLogger("RPST")
|
||||
|
||||
|
||||
def write_post_data(post_data: dict, filename: str, args, tree_branch: Tree):
|
||||
"""
|
||||
Writes post data to a specified JSON or CSV file based on the args provided, and updates
|
||||
the provided tree with the status.
|
||||
|
||||
:param post_data: A dictionary containing post data.
|
||||
:param filename: The name of the file to which post data will be written.
|
||||
:param args: A namespace object from argparse containing the output format options (args.json or args.csv).
|
||||
:param tree_branch: A rich Tree object to which status information will be added.
|
||||
"""
|
||||
home_directory = os.path.expanduser("~")
|
||||
|
||||
if args.json:
|
||||
json_file_path = os.path.join(home_directory, f"{filename}.json")
|
||||
with open(json_file_path, "a", encoding="utf-8") as file:
|
||||
file.write(json.dumps(post_data, ensure_ascii=False))
|
||||
file.write("\n") # Separate posts with newline
|
||||
tree_branch.add(
|
||||
f"{glyph.page_facing_up} JSON data successfully written/appended to file: "
|
||||
f"[italic][link file://{json_file_path}]{json_file_path}[/]"
|
||||
)
|
||||
else:
|
||||
tree_branch.add(
|
||||
f"{glyph.cross_mark_button} JSON data writing operation was skipped. No changes made."
|
||||
)
|
||||
|
||||
if args.csv:
|
||||
csv_file_path = os.path.join(home_directory, f"{filename}.csv")
|
||||
with open(csv_file_path, "a", newline="", encoding="utf-8") as csvfile:
|
||||
writer = csv.DictWriter(csvfile, fieldnames=post_data.keys())
|
||||
|
||||
# Write headers if file is empty
|
||||
if csvfile.tell() == 0:
|
||||
writer.writeheader()
|
||||
|
||||
writer.writerow(post_data)
|
||||
tree_branch.add(
|
||||
f"{glyph.page_facing_up} CSV data successfully written/appended to file: "
|
||||
f"[italic][link file://{csv_file_path}]{csv_file_path}[/]"
|
||||
)
|
||||
else:
|
||||
tree_branch.add(
|
||||
f"{glyph.cross_mark_button} CSV data writing operation was skipped. No changes made."
|
||||
)
|
||||