diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3ad89e2..00d017e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,7 +8,7 @@ updates: - package-ecosystem: "nuget" schedule: interval: "daily" - directory: "Reddit Post Scraping Tool" + directory: "RPST GUI" ignore: - dependency-name: "Newtonsoft.Json" - package-ecosystem: "pip" diff --git a/Dockerfile b/Dockerfile index 69e9374..5590f88 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,6 @@ WORKDIR /app COPY . . -RUN pip install --upgrade pip && pip install build && python -m build && pip install dist/*.whl +RUN pip install --upgrade pip && pip install . -ENTRYPOINT ["reddit_post_scraping_tool"] \ No newline at end of file +ENTRYPOINT ["rpst"] diff --git a/README.md b/README.md index 063a562..15f9871 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,13 @@ Given a subreddit name and a keyword, this script will return all posts from a s # Features (GUI) -- [x] Auto dark mode from 6pm - 6am -- [x] Saves results to a JSON +- [x] Dark mode (Right-click) +- [x] Saves results to a JSON (Right-click) - [ ] Other features coming soon... # TODO (GUI) - [ ] Make it a stand alone executable -- [ ] Add manual dark mode option, that will be remembered in all sessions +- [x] Add manual dark mode option, that will be remembered in all sessions - [ ] Make it save results to a CSV file # Wiki @@ -25,6 +25,6 @@ Given a subreddit name and a keyword, this script will return all posts from a s # Donations If you like `Reddit Post Scraping Tool` and would like to show support, you can Buy A Coffee for the developer using the button below -Buy Me A Coffee +Buy Me A Coffee Your support will be much appreciated😊 diff --git a/Reddit Post Scraping Tool/Reddit Post Scraping Tool.sln b/RPST GUI/RPST.sln similarity index 82% rename from Reddit Post Scraping Tool/Reddit Post Scraping Tool.sln rename to RPST GUI/RPST.sln index 8768cbe..10c08d0 100644 --- a/Reddit Post Scraping Tool/Reddit Post Scraping Tool.sln +++ b/RPST GUI/RPST.sln @@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.4.33213.308 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "Reddit Post Scraping Tool", "Reddit Post Scraping Tool\Reddit Post Scraping Tool.vbproj", "{46C2541E-6F65-461A-A479-F65D445C36EA}" +Project("{778DAE3C-4631-46EA-AA77-85C1314464D9}") = "RPST", "RPST\RPST.vbproj", "{46C2541E-6F65-461A-A479-F65D445C36EA}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/Reddit Post Scraping Tool/Reddit Post Scraping Tool/AboutForm.Designer.vb b/RPST GUI/RPST/AboutBox.Designer.vb similarity index 70% rename from Reddit Post Scraping Tool/Reddit Post Scraping Tool/AboutForm.Designer.vb rename to RPST GUI/RPST/AboutBox.Designer.vb index f935848..c2095fb 100644 --- a/Reddit Post Scraping Tool/Reddit Post Scraping Tool/AboutForm.Designer.vb +++ b/RPST GUI/RPST/AboutBox.Designer.vb @@ -1,5 +1,5 @@  _ -Partial Class AboutForm +Partial Class AboutBox Inherits System.Windows.Forms.Form 'Form overrides dispose to clean up the component list. @@ -22,11 +22,11 @@ Partial Class AboutForm 'Do not modify it using the code editor. _ Private Sub InitializeComponent() - Dim resources As ComponentModel.ComponentResourceManager = New ComponentModel.ComponentResourceManager(GetType(AboutForm)) + Dim resources As ComponentModel.ComponentResourceManager = New ComponentModel.ComponentResourceManager(GetType(AboutBox)) PictureBox1 = New PictureBox() LicenseRichTextBox = New RichTextBox() - Label1 = New Label() - Label2 = New Label() + LicenseLabel = New Label() + ProgramNameLabel = New Label() DescriptionLabel = New Label() VersionLabel = New Label() WikiLinkLabel = New LinkLabel() @@ -51,33 +51,37 @@ Partial Class AboutForm LicenseRichTextBox.ReadOnly = True LicenseRichTextBox.Size = New Size(513, 342) LicenseRichTextBox.TabIndex = 1 - LicenseRichTextBox.Text = ""' - ' Label1 + LicenseRichTextBox.Text = "License notice" ' - Label1.AutoSize = True - Label1.Font = New Font("Microsoft JhengHei UI", 9.75F, FontStyle.Regular, GraphicsUnit.Point) - Label1.Location = New Point(28, 150) - Label1.Name = "Label1" - Label1.Size = New Size(52, 17) - Label1.TabIndex = 2 - Label1.Text = "License"' - ' Label2 + ' LicenseLabel + ' + LicenseLabel.AutoSize = True + LicenseLabel.Font = New Font("Microsoft JhengHei UI", 9.75F, FontStyle.Regular, GraphicsUnit.Point) + LicenseLabel.Location = New Point(28, 150) + LicenseLabel.Name = "LicenseLabel" + LicenseLabel.Size = New Size(52, 17) + LicenseLabel.TabIndex = 2 + LicenseLabel.Text = "License" + ' + ' ProgramNameLabel + ' + ProgramNameLabel.AutoSize = True + ProgramNameLabel.Font = New Font("Microsoft JhengHei", 14.25F, FontStyle.Bold, GraphicsUnit.Point) + ProgramNameLabel.Location = New Point(126, 47) + ProgramNameLabel.Name = "ProgramNameLabel" + ProgramNameLabel.Size = New Size(66, 24) + ProgramNameLabel.TabIndex = 3 + ProgramNameLabel.Text = "Name" ' - Label2.AutoSize = True - Label2.Font = New Font("Microsoft JhengHei", 14.25F, FontStyle.Bold, GraphicsUnit.Point) - Label2.Location = New Point(126, 47) - Label2.Name = "Label2" - Label2.Size = New Size(246, 24) - Label2.TabIndex = 3 - Label2.Text = "Reddit Post Scraping Tool"' ' DescriptionLabel ' DescriptionLabel.AutoSize = True DescriptionLabel.Font = New Font("Segoe UI", 9F, FontStyle.Regular, GraphicsUnit.Point) DescriptionLabel.Location = New Point(126, 81) DescriptionLabel.Name = "DescriptionLabel" - DescriptionLabel.Size = New Size(0, 15) + DescriptionLabel.Size = New Size(67, 15) DescriptionLabel.TabIndex = 4 + DescriptionLabel.Text = "Description" ' ' VersionLabel ' @@ -85,9 +89,10 @@ Partial Class AboutForm VersionLabel.Font = New Font("Segoe UI", 9F, FontStyle.Italic, GraphicsUnit.Point) VersionLabel.Location = New Point(500, 54) VersionLabel.Name = "VersionLabel" - VersionLabel.Size = New Size(42, 15) + VersionLabel.Size = New Size(46, 15) VersionLabel.TabIndex = 5 - VersionLabel.Text = "Label4"' + VersionLabel.Text = "Version" + ' ' WikiLinkLabel ' WikiLinkLabel.AutoSize = True @@ -96,8 +101,9 @@ Partial Class AboutForm WikiLinkLabel.Size = New Size(30, 15) WikiLinkLabel.TabIndex = 6 WikiLinkLabel.TabStop = True - WikiLinkLabel.Text = "Wiki"' - ' AboutForm + WikiLinkLabel.Text = "Wiki" + ' + ' AboutBox ' AutoScaleDimensions = New SizeF(7F, 15F) AutoScaleMode = AutoScaleMode.Font @@ -106,18 +112,18 @@ Partial Class AboutForm Controls.Add(WikiLinkLabel) Controls.Add(VersionLabel) Controls.Add(DescriptionLabel) - Controls.Add(Label2) - Controls.Add(Label1) + Controls.Add(ProgramNameLabel) + Controls.Add(LicenseLabel) Controls.Add(LicenseRichTextBox) Controls.Add(PictureBox1) FormBorderStyle = FormBorderStyle.FixedSingle Icon = CType(resources.GetObject("$this.Icon"), Icon) MaximizeBox = False MinimizeBox = False - Name = "AboutForm" + Name = "AboutBox" ShowInTaskbar = False StartPosition = FormStartPosition.CenterScreen - Text = "About - Reddit Post Scraping Tool" + Text = "About" CType(PictureBox1, ComponentModel.ISupportInitialize).EndInit() ResumeLayout(False) PerformLayout() @@ -125,8 +131,8 @@ Partial Class AboutForm Friend WithEvents PictureBox1 As PictureBox Friend WithEvents LicenseRichTextBox As RichTextBox - Friend WithEvents Label1 As Label - Friend WithEvents Label2 As Label + Friend WithEvents LicenseLabel As Label + Friend WithEvents ProgramNameLabel As Label Friend WithEvents DescriptionLabel As Label Friend WithEvents VersionLabel As Label Friend WithEvents WikiLinkLabel As LinkLabel diff --git a/Reddit Post Scraping Tool/Reddit Post Scraping Tool/AboutForm.resx b/RPST GUI/RPST/AboutBox.resx similarity index 94% rename from Reddit Post Scraping Tool/Reddit Post Scraping Tool/AboutForm.resx rename to RPST GUI/RPST/AboutBox.resx index 386960c..ba2c572 100644 --- a/Reddit Post Scraping Tool/Reddit Post Scraping Tool/AboutForm.resx +++ b/RPST GUI/RPST/AboutBox.resx @@ -1,4 +1,64 @@ - + + + @@ -61,7 +121,7 @@ iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAYAAAD0eNT6AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAN - 0AAADdABEGw9BwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAFPuSURBVHhe7d0J + 0wAADdMBvdUcagAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAFPuSURBVHhe7d0J nBxVvTZ+ExbZVPTPX0Svonjd5b0qV0UEARXR60Xvq2yyi4DsskR2QfZNEEE2gbAIAgEEAgQIS4BAQEh6 uqfXmUkySaYHbyCsCXuSep9fJyOdzjMz3dW1nKp++Hy+H+AH6e4659T5nao6dc57PM8TERGRDkODIiIi km40KCIiIulGgyIiIpJuNCgiIiLpRoMiIiKSbjQoIiIi6UaDIiIikm40KCIiIulGgyIiIpJuNCgiIiLp diff --git a/RPST GUI/RPST/AboutBox.vb b/RPST GUI/RPST/AboutBox.vb new file mode 100644 index 0000000..a8ebb19 --- /dev/null +++ b/RPST GUI/RPST/AboutBox.vb @@ -0,0 +1,36 @@ +Public Class AboutBox + Public Property LicenseText As String = "MIT License + +Copyright (c) 2023-2024 Richard Mwewa + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the ""Software""), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE." + + Private Sub AboutForm_Load(sender As Object, e As EventArgs) Handles MyBase.Load + ProgramNameLabel.Text = My.Application.Info.AssemblyName + DescriptionLabel.Text = "Given a subreddit name and a keyword, +RPST returns all top posts +(by default) that contain the specified keyword." + VersionLabel.Text = $"v{My.Application.Info.Version}" + LicenseRichTextBox.Text = LicenseText + End Sub + + Private Sub LinkLabel1_LinkClicked(sender As Object, e As LinkLabelLinkClickedEventArgs) Handles WikiLinkLabel.LinkClicked + Shell("cmd /c start https://github.com/bellingcat/reddit-post-scraping-tool/wiki") + End Sub +End Class \ No newline at end of file diff --git a/RPST GUI/RPST/ApiHandler.vb b/RPST GUI/RPST/ApiHandler.vb new file mode 100644 index 0000000..7ed7032 --- /dev/null +++ b/RPST GUI/RPST/ApiHandler.vb @@ -0,0 +1,50 @@ +Imports System.IO +Imports System.Net.Http +Imports Newtonsoft.Json +Imports Newtonsoft.Json.Linq + +''' +''' Handles requests to Reddit and Github APIs. +''' +Public Class ApiHandler + Public Property LogFile As String = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RedditPostScrapingTool", "logs", $"debug.log") + Public Property Headers As String = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1.2 Safari/605.1.15" + Public Property UpdatesEndpoint As String = "https://api.github.com/repos/bellingcat/reddit-post-scraping-tool/releases/latest" + + ''' + ''' Scrape Reddit data. + ''' + ''' Json object containing scraped data. + Public Function ScrapeReddit(subreddit As String, listing As String, limit As Integer, timeframe As String) As JObject + Dim ApiEndpoint As String = $"https://reddit.com/r/{subreddit}/{listing}.json?limit={limit}&t={timeframe}" + Return GetJObjectFromEndpoint(ApiEndpoint) + End Function + + ''' + ''' Gets remote version information from the repository release page. + ''' + ''' Json object containing update data. + Public Function CheckUpdates() As JObject + Return GetJObjectFromEndpoint(UpdatesEndpoint) + End Function + + Private Function GetJObjectFromEndpoint(endpoint As String) As JObject + Try + Using httpClient As New HttpClient() + httpClient.DefaultRequestHeaders.Add("User-Agent", headers) + Dim response As HttpResponseMessage = httpClient.GetAsync(endpoint).Result + If response.IsSuccessStatusCode Then + Dim json As String = response.Content.ReadAsStringAsync().Result + Dim data As JObject = JsonConvert.DeserializeObject(Of JObject)(json) + Return data + Else + MessageBox.Show(response.ReasonPhrase, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error) + End If + End Using + Catch ex As Exception + My.Computer.FileSystem.WriteAllText(LogFile, $"{DateTime.Now}: {ex}{Environment.NewLine}", True) + MessageBox.Show($"{ex.Message}. Please see the debug log '{LogFile}' for more information.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error) + End Try + Return New JObject() + End Function +End Class diff --git a/Reddit Post Scraping Tool/Reddit Post Scraping Tool/ApplicationEvents.vb b/RPST GUI/RPST/ApplicationEvents.vb similarity index 100% rename from Reddit Post Scraping Tool/Reddit Post Scraping Tool/ApplicationEvents.vb rename to RPST GUI/RPST/ApplicationEvents.vb diff --git a/RPST GUI/RPST/DataGridViewHandler.vb b/RPST GUI/RPST/DataGridViewHandler.vb new file mode 100644 index 0000000..6816832 --- /dev/null +++ b/RPST GUI/RPST/DataGridViewHandler.vb @@ -0,0 +1,67 @@ +Imports Newtonsoft.Json.Linq + +Public Class DataGridViewHandler + ''' + ''' Initializes the DataGridView by clearing any existing data and setting up the necessary columns. + ''' + ''' The DataGridView to be initialized. + Public Shared Sub AddColumn(dataGridView As DataGridView) + ' Clear the Columns and Rows before adding Items to them + PostsForm.DataGridViewPosts.Rows.Clear() + PostsForm.DataGridViewPosts.Columns.Clear() + + PostsForm.DataGridViewPosts.Columns.Add("PostCount", "Post Number") + PostsForm.DataGridViewPosts.Columns.Add("PostAuthor", "Author") + PostsForm.DataGridViewPosts.Columns.Add("PostID", "ID") + PostsForm.DataGridViewPosts.Columns.Add("PostSubreddit", "Subreddit") + PostsForm.DataGridViewPosts.Columns.Add("SubredditVisibility", "Subreddit Visibility") + PostsForm.DataGridViewPosts.Columns.Add("PostThumbnail", "Thumbnail") + PostsForm.DataGridViewPosts.Columns.Add("PostIsNSFW", "NSFW") + PostsForm.DataGridViewPosts.Columns.Add("PostIsGilded", "Gilded") + PostsForm.DataGridViewPosts.Columns.Add("PostUpvotes", "Upvotes") + PostsForm.DataGridViewPosts.Columns.Add("PostUpvoteRatio", "Upvote Ratio") + PostsForm.DataGridViewPosts.Columns.Add("PostDownvotes", "Downvotes") + PostsForm.DataGridViewPosts.Columns.Add("PostAwards", "Awards") + PostsForm.DataGridViewPosts.Columns.Add("PostTopAward", "Top Award") + PostsForm.DataGridViewPosts.Columns.Add("PostIsCrosspostable", "Is Crosspostable?") + PostsForm.DataGridViewPosts.Columns.Add("PostScore", "Score") + PostsForm.DataGridViewPosts.Columns.Add("PostText", "Text") + PostsForm.DataGridViewPosts.Columns.Add("PostCategory", "Category") + PostsForm.DataGridViewPosts.Columns.Add("PostDomain", "Domain") + PostsForm.DataGridViewPosts.Columns.Add("PostPermalink", "Permalink") + PostsForm.DataGridViewPosts.Columns.Add("PostCreatedAt", "Created At") + PostsForm.DataGridViewPosts.Columns.Add("PostApprovedAt", "Approved At") + PostsForm.DataGridViewPosts.Columns.Add("PostApprovedBy", "Approved By") + End Sub + + Public Shared Sub AddRow(dataGridView As DataGridView, post As JObject, postNumber As Integer) + ''' + ''' Adds a row to the DataGridView based on the data from a Reddit post. + ''' + ''' The DataGridView to which the row will be added. + ''' A JObject representing the Reddit post. + ''' The number of the post. + PostsForm.DataGridViewPosts.Rows.Add(postNumber, + post("data")("author"), + post("data")("id"), + post("data")("subreddit_name_prefixed"), + post("data")("subreddit_type"), + post("data")("thumbnail"), + post("data")("over_18"), + post("data")("gilded"), + post("data")("ups"), + post("data")("upvote_ratio"), + post("data")("downs"), + post("data")("total_awards_received"), + post("data")("top_awarded_type"), + post("data")("is_crosspostable"), + post("data")("score"), + post("data")("selftext"), + post("data")("category"), + post("data")("domain"), + post("data")("permalink"), + post("data")("created"), + post("data")("approved_at_utc"), + post("data")("approved_by")) + End Sub +End Class diff --git a/RPST GUI/RPST/DeveloperForm.Designer.vb b/RPST GUI/RPST/DeveloperForm.Designer.vb new file mode 100644 index 0000000..db4173f --- /dev/null +++ b/RPST GUI/RPST/DeveloperForm.Designer.vb @@ -0,0 +1,84 @@ + +Partial Class DeveloperForm + Inherits System.Windows.Forms.Form + + 'Form overrides dispose to clean up the component list. + + 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. + + Private Sub InitializeComponent() + Dim resources As ComponentModel.ComponentResourceManager = New ComponentModel.ComponentResourceManager(GetType(DeveloperForm)) + AboutMeLinkLabel = New LinkLabel() + BuyMeACoffeeLinkLabel = New LinkLabel() + GreetingLabel = New Label() + SuspendLayout() + ' + ' AboutMeLinkLabel + ' + AboutMeLinkLabel.AutoSize = True + AboutMeLinkLabel.BackColor = Color.White + AboutMeLinkLabel.Location = New Point(33, 426) + AboutMeLinkLabel.Name = "AboutMeLinkLabel" + AboutMeLinkLabel.Size = New Size(60, 15) + AboutMeLinkLabel.TabIndex = 0 + AboutMeLinkLabel.TabStop = True + AboutMeLinkLabel.Text = "About.me"' + ' BuyMeACoffeeLinkLabel + ' + BuyMeACoffeeLinkLabel.AutoSize = True + BuyMeACoffeeLinkLabel.Location = New Point(33, 451) + BuyMeACoffeeLinkLabel.Name = "BuyMeACoffeeLinkLabel" + BuyMeACoffeeLinkLabel.Size = New Size(96, 15) + BuyMeACoffeeLinkLabel.TabIndex = 1 + BuyMeACoffeeLinkLabel.TabStop = True + BuyMeACoffeeLinkLabel.Text = "Buy Me A Coffee"' + ' GreetingLabel + ' + GreetingLabel.AutoSize = True + GreetingLabel.Font = New Font("Verdana", 27.75F, FontStyle.Bold, GraphicsUnit.Point) + GreetingLabel.Location = New Point(62, 22) + GreetingLabel.Name = "GreetingLabel" + GreetingLabel.Size = New Size(382, 45) + GreetingLabel.TabIndex = 3 + GreetingLabel.Text = "Hello, I'm Ritchie"' + ' DeveloperForm + ' + AutoScaleDimensions = New SizeF(7F, 15F) + AutoScaleMode = AutoScaleMode.Font + BackgroundImage = CType(resources.GetObject("$this.BackgroundImage"), Image) + ClientSize = New Size(510, 510) + Controls.Add(BuyMeACoffeeLinkLabel) + Controls.Add(AboutMeLinkLabel) + Controls.Add(GreetingLabel) + FormBorderStyle = FormBorderStyle.FixedSingle + MaximizeBox = False + MinimizeBox = False + Name = "DeveloperForm" + ShowIcon = False + ShowInTaskbar = False + StartPosition = FormStartPosition.CenterParent + Text = "Developer" + ResumeLayout(False) + PerformLayout() + End Sub + + Friend WithEvents AboutMeLinkLabel As LinkLabel + Friend WithEvents BuyMeACoffeeLinkLabel As LinkLabel + Friend WithEvents PictureBox1 As PictureBox + Friend WithEvents GreetingLabel As Label +End Class diff --git a/Reddit Post Scraping Tool/Reddit Post Scraping Tool/DeveloperForm.resx b/RPST GUI/RPST/DeveloperForm.resx similarity index 100% rename from Reddit Post Scraping Tool/Reddit Post Scraping Tool/DeveloperForm.resx rename to RPST GUI/RPST/DeveloperForm.resx diff --git a/Reddit Post Scraping Tool/Reddit Post Scraping Tool/DeveloperForm.vb b/RPST GUI/RPST/DeveloperForm.vb similarity index 90% rename from Reddit Post Scraping Tool/Reddit Post Scraping Tool/DeveloperForm.vb rename to RPST GUI/RPST/DeveloperForm.vb index 43cc083..007cc52 100644 --- a/Reddit Post Scraping Tool/Reddit Post Scraping Tool/DeveloperForm.vb +++ b/RPST GUI/RPST/DeveloperForm.vb @@ -12,6 +12,6 @@ End Sub Private Sub BuyMeACoffeeLinkLabel_LinkClicked(sender As Object, e As LinkLabelLinkClickedEventArgs) Handles BuyMeACoffeeLinkLabel.LinkClicked - Shell("cmd /c start https://buymeacoffee.com/189381184") + Shell("cmd /c start https://buymeacoffee.com/_rly0nheart") End Sub End Class \ No newline at end of file diff --git a/Reddit Post Scraping Tool/Reddit Post Scraping Tool/LICENSE b/RPST GUI/RPST/LICENSE similarity index 100% rename from Reddit Post Scraping Tool/Reddit Post Scraping Tool/LICENSE rename to RPST GUI/RPST/LICENSE diff --git a/Reddit Post Scraping Tool/Reddit Post Scraping Tool/My Project/Application.Designer.vb b/RPST GUI/RPST/My Project/Application.Designer.vb similarity index 93% rename from Reddit Post Scraping Tool/Reddit Post Scraping Tool/My Project/Application.Designer.vb rename to RPST GUI/RPST/My Project/Application.Designer.vb index be8f71d..e4e5f96 100644 --- a/Reddit Post Scraping Tool/Reddit Post Scraping Tool/My Project/Application.Designer.vb +++ b/RPST GUI/RPST/My Project/Application.Designer.vb @@ -33,7 +33,7 @@ Namespace My _ Protected Overrides Sub OnCreateMainForm() - Me.MainForm = Global.Reddit_Post_Scraping_Tool.StartForm + Me.MainForm = Global.RPST.StartForm End Sub End Class End Namespace diff --git a/Reddit Post Scraping Tool/Reddit Post Scraping Tool/My Project/Application.myapp b/RPST GUI/RPST/My Project/Application.myapp similarity index 100% rename from Reddit Post Scraping Tool/Reddit Post Scraping Tool/My Project/Application.myapp rename to RPST GUI/RPST/My Project/Application.myapp diff --git a/Reddit Post Scraping Tool/Reddit Post Scraping Tool/My Project/Resources.Designer.vb b/RPST GUI/RPST/My Project/Resources.Designer.vb similarity index 93% rename from Reddit Post Scraping Tool/Reddit Post Scraping Tool/My Project/Resources.Designer.vb rename to RPST GUI/RPST/My Project/Resources.Designer.vb index 85e6e82..cc0c0d8 100644 --- a/Reddit Post Scraping Tool/Reddit Post Scraping Tool/My Project/Resources.Designer.vb +++ b/RPST GUI/RPST/My Project/Resources.Designer.vb @@ -39,7 +39,7 @@ Namespace My.Resources Friend ReadOnly Property ResourceManager() As Global.System.Resources.ResourceManager Get If Object.ReferenceEquals(resourceMan, Nothing) Then - Dim temp As Global.System.Resources.ResourceManager = New Global.System.Resources.ResourceManager("Reddit_Post_Scraping_Tool.Resources", GetType(Resources).Assembly) + Dim temp As Global.System.Resources.ResourceManager = New Global.System.Resources.ResourceManager("RPST.Resources", GetType(Resources).Assembly) resourceMan = temp End If Return resourceMan diff --git a/Reddit Post Scraping Tool/Reddit Post Scraping Tool/My Project/Resources.resx b/RPST GUI/RPST/My Project/Resources.resx similarity index 100% rename from Reddit Post Scraping Tool/Reddit Post Scraping Tool/My Project/Resources.resx rename to RPST GUI/RPST/My Project/Resources.resx diff --git a/RPST GUI/RPST/PostsForm.Designer.vb b/RPST GUI/RPST/PostsForm.Designer.vb new file mode 100644 index 0000000..40c8ecb --- /dev/null +++ b/RPST GUI/RPST/PostsForm.Designer.vb @@ -0,0 +1,57 @@ + _ +Partial Class PostsForm + Inherits System.Windows.Forms.Form + + 'Form overrides dispose to clean up the component list. + _ + 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. + _ + Private Sub InitializeComponent() + DataGridViewPosts = New DataGridView() + CType(DataGridViewPosts, ComponentModel.ISupportInitialize).BeginInit() + SuspendLayout() + ' + ' DataGridViewPosts + ' + DataGridViewPosts.BackgroundColor = Color.Gainsboro + DataGridViewPosts.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize + DataGridViewPosts.Dock = DockStyle.Fill + DataGridViewPosts.Location = New Point(0, 0) + DataGridViewPosts.Name = "DataGridViewPosts" + DataGridViewPosts.ReadOnly = True + DataGridViewPosts.RowHeadersVisible = False + DataGridViewPosts.RowTemplate.Height = 25 + DataGridViewPosts.Size = New Size(800, 450) + DataGridViewPosts.TabIndex = 3 + ' + ' PostsForm + ' + AutoScaleDimensions = New SizeF(7F, 15F) + AutoScaleMode = AutoScaleMode.Font + ClientSize = New Size(800, 450) + Controls.Add(DataGridViewPosts) + Name = "PostsForm" + ShowIcon = False + ShowInTaskbar = False + Text = "PostsForm" + CType(DataGridViewPosts, ComponentModel.ISupportInitialize).EndInit() + ResumeLayout(False) + End Sub + + Friend WithEvents DataGridViewPosts As DataGridView +End Class diff --git a/Reddit Post Scraping Tool/Reddit Post Scraping Tool/PostsForm.resx b/RPST GUI/RPST/PostsForm.resx similarity index 100% rename from Reddit Post Scraping Tool/Reddit Post Scraping Tool/PostsForm.resx rename to RPST GUI/RPST/PostsForm.resx diff --git a/Reddit Post Scraping Tool/Reddit Post Scraping Tool/PostsForm.vb b/RPST GUI/RPST/PostsForm.vb similarity index 100% rename from Reddit Post Scraping Tool/Reddit Post Scraping Tool/PostsForm.vb rename to RPST GUI/RPST/PostsForm.vb diff --git a/RPST GUI/RPST/PostsProcessor.vb b/RPST GUI/RPST/PostsProcessor.vb new file mode 100644 index 0000000..1fef921 --- /dev/null +++ b/RPST GUI/RPST/PostsProcessor.vb @@ -0,0 +1,29 @@ +Imports Newtonsoft.Json.Linq + +Public Class PostsProcessor + Private ApiHandler As New ApiHandler + + ''' + ''' Fetches Reddit posts based on the given parameters and returns them as a JObject. + ''' + ''' The subreddit to fetch posts from. + ''' The type of listing (e.g., "new", "top", etc.). + ''' The maximum number of posts to fetch. + ''' The timeframe to consider for the posts (e.g., "day", "week", "month", "year", "all"). + ''' A JObject containing the fetched Reddit posts. + Public Function FetchPosts(subreddit As String, listing As String, limit As Integer, timeframe As String) As JObject + Dim posts As JObject = ApiHandler.ScrapeReddit(subreddit, listing, limit, timeframe) + Return posts + End Function + + ''' + ''' Checks if the given Reddit post contains the given keyword in its text. + ''' + ''' The Reddit post to check. + ''' The keyword to check for. + ''' True if the post contains the keyword, False otherwise. + Public Shared Function PostContainsKeyword(post As JObject, keyword As String) As Boolean + Return post("data")("selftext").ToString.ToLower(System.Globalization.CultureInfo.InvariantCulture).Contains(keyword.ToLower(System.Globalization.CultureInfo.InvariantCulture)) + End Function + +End Class diff --git a/Reddit Post Scraping Tool/Reddit Post Scraping Tool/README.md b/RPST GUI/RPST/README.md similarity index 100% rename from Reddit Post Scraping Tool/Reddit Post Scraping Tool/README.md rename to RPST GUI/RPST/README.md diff --git a/Reddit Post Scraping Tool/Reddit Post Scraping Tool/Reddit Post Scraping Tool.vbproj b/RPST GUI/RPST/RPST.vbproj similarity index 73% rename from Reddit Post Scraping Tool/Reddit Post Scraping Tool/Reddit Post Scraping Tool.vbproj rename to RPST GUI/RPST/RPST.vbproj index 695b48c..0c08e75 100644 --- a/Reddit Post Scraping Tool/Reddit Post Scraping Tool/Reddit Post Scraping Tool.vbproj +++ b/RPST GUI/RPST/RPST.vbproj @@ -3,23 +3,29 @@ WinExe net6.0-windows - Reddit_Post_Scraping_Tool.My.MyApplication + RPST.My.MyApplication true WindowsForms icon.ico - Richard Mwewa - Given a subreddit name and a keyword, this program returns all top (by default) posts that contain the specified keyword. - Copyright (c) 2023 Richard Mwewa. All rights reserved. + Bellingcat + Given a subreddit name and a keyword, RPST (Reddit Post Scraping Tool) returns all top (by default) posts that contain the specified keyword. + Copyright (c) 2023-2024 Richard Mwewa https://github.com/bellingcat/reddit-post-scraping-tool README.md https://github.com/bellingcat/reddit-post-scraping-tool - 1.3.0.1 - 1.3.0.1 + 1.4.0.0 + 1.4.0.0 LICENSE True - 1.3.0 + 1.4.0 reddit;scraper;reddit-scraper;osint + 6.0-recommended + RPST (Reddit Post Scraping Tool) + Richard Mwewa + en + $(AssemblyName) + RPST (Reddit Post Scraping Tool) diff --git a/Reddit Post Scraping Tool/Reddit Post Scraping Tool/Reddit Post Scraping Tool.vbproj.user b/RPST GUI/RPST/RPST.vbproj.user similarity index 89% rename from Reddit Post Scraping Tool/Reddit Post Scraping Tool/Reddit Post Scraping Tool.vbproj.user rename to RPST GUI/RPST/RPST.vbproj.user index 9678d0f..702f8a2 100644 --- a/Reddit Post Scraping Tool/Reddit Post Scraping Tool/Reddit Post Scraping Tool.vbproj.user +++ b/RPST GUI/RPST/RPST.vbproj.user @@ -1,7 +1,7 @@  - + Form diff --git a/RPST GUI/RPST/Settings.vb b/RPST GUI/RPST/Settings.vb new file mode 100644 index 0000000..7230860 --- /dev/null +++ b/RPST GUI/RPST/Settings.vb @@ -0,0 +1,111 @@ +Imports Newtonsoft.Json +Imports Newtonsoft.Json.Linq +Imports System.IO +Imports System.Runtime +Imports System.Text.Json + +Public Class SettingsManager + + Public Property DarkMode As Boolean + Private ReadOnly settingsFilePath As String = Path.Combine(Environment.CurrentDirectory, "settings.json") + + + ' Load settings from the 'settings.json' file + Public Sub LoadSettings() + ' Check if the settings.json file exists + ' and load the configurations from it + If File.Exists(settingsFilePath) Then + Dim json As String = File.ReadAllText(settingsFilePath) + Dim options As New JsonSerializerOptions With {.PropertyNameCaseInsensitive = True} + Dim settings = Text.Json.JsonSerializer.Deserialize(Of SettingsManager)(json, options) + Me.DarkMode = settings.DarkMode + StartForm.DarkModeToolStripMenuItem.Checked = settings.DarkMode + Else + ' Settings file does not exist + ' Create a new file with default settings 'False' + Dim defaultSettings = New SettingsManager With {.DarkMode = False} + Dim jsonOutput = Text.Json.JsonSerializer.Serialize(defaultSettings) + File.WriteAllText(settingsFilePath, jsonOutput) + + Me.DarkMode = False + StartForm.DarkModeToolStripMenuItem.Checked = False + End If + End Sub + + + ' Toggle Dark mode + Public Sub ToggleDarkMode(enabled As Boolean) + Dim json As String = File.ReadAllText(settingsFilePath) + Dim options As New JsonSerializerOptions With {.PropertyNameCaseInsensitive = True} + Dim settings As SettingsManager = Text.Json.JsonSerializer.Deserialize(Of SettingsManager)(json, options) + settings.DarkMode = enabled + SaveSettings(settings) + ApplyTheme() + End Sub + + + ' Save current settings to settings.json + Private Sub SaveSettings(settings) + Dim jsonOutput = Text.Json.JsonSerializer.Serialize(settings) + File.WriteAllText(settingsFilePath, jsonOutput) + End Sub + + + ' Apply theme + Public Sub ApplyTheme() + Dim DarkMode As Boolean = GetDarkMode() + If DarkMode Then + StartForm.BackColor = ColorTranslator.FromHtml("#FF121212") + StartForm.ToolsToolStripMenuTools.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF") + StartForm.KeywordTextBox.BackColor = ColorTranslator.FromHtml("#FF2E2E2E") + StartForm.KeywordTextBox.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF") + StartForm.SubredditTextBox.BackColor = ColorTranslator.FromHtml("#FF2E2E2E") + StartForm.SubredditTextBox.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF") + StartForm.LimitNumericUpDown.BackColor = ColorTranslator.FromHtml("#FF2E2E2E") + StartForm.LimitNumericUpDown.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF") + StartForm.LimitNumericUpDown.BackColor = ColorTranslator.FromHtml("#FF2E2E2E") + StartForm.LimitNumericUpDown.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF") + StartForm.ListingComboBox.BackColor = ColorTranslator.FromHtml("#FF2E2E2E") + StartForm.ListingComboBox.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF") + StartForm.TimeframeComboBox.BackColor = ColorTranslator.FromHtml("#FF2E2E2E") + StartForm.TimeframeComboBox.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF") + StartForm.Label1.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF") + StartForm.Label2.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF") + StartForm.Label3.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF") + StartForm.Label4.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF") + StartForm.Label5.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF") + Else + StartForm.BackColor = ColorTranslator.FromHtml("#FFFFFFFF") + StartForm.ToolsToolStripMenuTools.ForeColor = ColorTranslator.FromHtml("#FF121212") + StartForm.KeywordTextBox.BackColor = ColorTranslator.FromHtml("#FFFFFFFF") + StartForm.KeywordTextBox.ForeColor = ColorTranslator.FromHtml("#FF121212") + StartForm.SubredditTextBox.BackColor = ColorTranslator.FromHtml("#FFFFFFFF") + StartForm.SubredditTextBox.ForeColor = ColorTranslator.FromHtml("#FF121212") + StartForm.LimitNumericUpDown.BackColor = ColorTranslator.FromHtml("#FFFFFFFF") + StartForm.LimitNumericUpDown.ForeColor = ColorTranslator.FromHtml("#FF121212") + StartForm.LimitNumericUpDown.BackColor = ColorTranslator.FromHtml("#FFFFFFFF") + StartForm.LimitNumericUpDown.ForeColor = ColorTranslator.FromHtml("#FF121212") + StartForm.ListingComboBox.BackColor = ColorTranslator.FromHtml("#FFFFFFFF") + StartForm.ListingComboBox.ForeColor = ColorTranslator.FromHtml("#FF121212") + StartForm.TimeframeComboBox.BackColor = ColorTranslator.FromHtml("#FFFFFFFF") + StartForm.TimeframeComboBox.ForeColor = ColorTranslator.FromHtml("#FF121212") + StartForm.Label1.ForeColor = ColorTranslator.FromHtml("#FF121212") + StartForm.Label2.ForeColor = ColorTranslator.FromHtml("#FF121212") + StartForm.Label3.ForeColor = ColorTranslator.FromHtml("#FF121212") + StartForm.Label4.ForeColor = ColorTranslator.FromHtml("#FF121212") + StartForm.Label5.ForeColor = ColorTranslator.FromHtml("#FF121212") + End If + End Sub + + ' Get dark mode state from settings.json's key 'DarkMode' + Private Function GetDarkMode() As Boolean + If File.Exists(settingsFilePath) Then + Dim json As String = File.ReadAllText(settingsFilePath) + Dim settings As JObject = JObject.Parse(json) + Return settings("DarkMode").ToObject(Of Boolean)() + Else + Return False + End If + End Function + +End Class \ No newline at end of file diff --git a/Reddit Post Scraping Tool/Reddit Post Scraping Tool/StartForm.Designer.vb b/RPST GUI/RPST/StartForm.Designer.vb similarity index 75% rename from Reddit Post Scraping Tool/Reddit Post Scraping Tool/StartForm.Designer.vb rename to RPST GUI/RPST/StartForm.Designer.vb index d646060..03a6051 100644 --- a/Reddit Post Scraping Tool/Reddit Post Scraping Tool/StartForm.Designer.vb +++ b/RPST GUI/RPST/StartForm.Designer.vb @@ -35,17 +35,19 @@ Partial Class StartForm Label4 = New Label() Label5 = New Label() ContextMenuStrip1 = New ContextMenuStrip(components) - SaveResultsJSONToolStripMenuItem = New ToolStripMenuItem() + SaveResultsStripMenuItem = New ToolStripMenuItem() JSONToolStripMenuItem = New ToolStripMenuItem() CSVToolStripMenuItem = New ToolStripMenuItem() + DarkModeToolStripMenuItem = New ToolStripMenuItem() FileMenuStrip = New MenuStrip() ToolsToolStripMenuTools = New ToolStripMenuItem() AboutToolStripMenuItem = New ToolStripMenuItem() DeveloperToolStripMenuItem = New ToolStripMenuItem() - ChekUpdatesToolStripMenuItem = New ToolStripMenuItem() + CheckUpdatesToolStripMenuItem = New ToolStripMenuItem() ToolStripSeparator2 = New ToolStripSeparator() QuitToolStripMenuItem = New ToolStripMenuItem() LimitNumericUpDown = New NumericUpDown() + ToolTip1 = New ToolTip(components) ContextMenuStrip1.SuspendLayout() FileMenuStrip.SuspendLayout() CType(LimitNumericUpDown, ComponentModel.ISupportInitialize).BeginInit() @@ -86,7 +88,8 @@ Partial Class StartForm TimeframeComboBox.Name = "TimeframeComboBox" TimeframeComboBox.Size = New Size(100, 23) TimeframeComboBox.TabIndex = 8 - TimeframeComboBox.Text = "All"' + TimeframeComboBox.Text = "All" + ' ' ListingComboBox ' ListingComboBox.FormattingEnabled = True @@ -95,7 +98,8 @@ Partial Class StartForm ListingComboBox.Name = "ListingComboBox" ListingComboBox.Size = New Size(100, 23) ListingComboBox.TabIndex = 9 - ListingComboBox.Text = "Top"' + ListingComboBox.Text = "Top" + ' ' Label1 ' Label1.AutoEllipsis = True @@ -105,7 +109,8 @@ Partial Class StartForm Label1.Name = "Label1" Label1.Size = New Size(56, 23) Label1.TabIndex = 10 - Label1.Text = "Keyword"' + Label1.Text = "Keyword" + ' ' Label2 ' Label2.AutoEllipsis = True @@ -115,7 +120,8 @@ Partial Class StartForm Label2.Name = "Label2" Label2.Size = New Size(63, 23) Label2.TabIndex = 11 - Label2.Text = "Subreddit"' + Label2.Text = "Subreddit" + ' ' Label3 ' Label3.AutoEllipsis = True @@ -125,7 +131,8 @@ Partial Class StartForm Label3.Name = "Label3" Label3.Size = New Size(56, 23) Label3.TabIndex = 12 - Label3.Text = "Limit"' + Label3.Text = "Limit" + ' ' Label4 ' Label4.AutoEllipsis = True @@ -135,7 +142,8 @@ Partial Class StartForm Label4.Name = "Label4" Label4.Size = New Size(56, 23) Label4.TabIndex = 13 - Label4.Text = "Listing"' + Label4.Text = "Listing" + ' ' Label5 ' Label5.AutoEllipsis = True @@ -145,22 +153,23 @@ Partial Class StartForm Label5.Name = "Label5" Label5.Size = New Size(71, 23) Label5.TabIndex = 14 - Label5.Text = "Timeframe"' + Label5.Text = "Timeframe" + ' ' ContextMenuStrip1 ' - ContextMenuStrip1.Items.AddRange(New ToolStripItem() {SaveResultsJSONToolStripMenuItem}) + ContextMenuStrip1.Items.AddRange(New ToolStripItem() {SaveResultsStripMenuItem, DarkModeToolStripMenuItem}) ContextMenuStrip1.Name = "ContextMenuStrip1" - ContextMenuStrip1.Size = New Size(144, 26) + ContextMenuStrip1.Size = New Size(144, 48) ' - ' SaveResultsJSONToolStripMenuItem + ' SaveResultsStripMenuItem + ' + SaveResultsStripMenuItem.AutoToolTip = True + SaveResultsStripMenuItem.DropDownItems.AddRange(New ToolStripItem() {JSONToolStripMenuItem, CSVToolStripMenuItem}) + SaveResultsStripMenuItem.Image = CType(resources.GetObject("SaveResultsStripMenuItem.Image"), Image) + SaveResultsStripMenuItem.Name = "SaveResultsStripMenuItem" + SaveResultsStripMenuItem.Size = New Size(143, 22) + SaveResultsStripMenuItem.Text = "Save posts to" ' - SaveResultsJSONToolStripMenuItem.AutoToolTip = True - SaveResultsJSONToolStripMenuItem.DropDownItems.AddRange(New ToolStripItem() {JSONToolStripMenuItem, CSVToolStripMenuItem}) - SaveResultsJSONToolStripMenuItem.Image = CType(resources.GetObject("SaveResultsJSONToolStripMenuItem.Image"), Image) - SaveResultsJSONToolStripMenuItem.Name = "SaveResultsJSONToolStripMenuItem" - SaveResultsJSONToolStripMenuItem.Size = New Size(143, 22) - SaveResultsJSONToolStripMenuItem.Text = "Save posts to" - SaveResultsJSONToolStripMenuItem.ToolTipText = "Save results to a JSON file"' ' JSONToolStripMenuItem ' JSONToolStripMenuItem.AutoToolTip = True @@ -168,7 +177,8 @@ Partial Class StartForm JSONToolStripMenuItem.Image = CType(resources.GetObject("JSONToolStripMenuItem.Image"), Image) JSONToolStripMenuItem.Name = "JSONToolStripMenuItem" JSONToolStripMenuItem.Size = New Size(185, 22) - JSONToolStripMenuItem.Text = "JSON"' + JSONToolStripMenuItem.Text = "JSON" + ' ' CSVToolStripMenuItem ' CSVToolStripMenuItem.AutoToolTip = True @@ -176,7 +186,17 @@ Partial Class StartForm CSVToolStripMenuItem.Image = CType(resources.GetObject("CSVToolStripMenuItem.Image"), Image) CSVToolStripMenuItem.Name = "CSVToolStripMenuItem" CSVToolStripMenuItem.Size = New Size(185, 22) - CSVToolStripMenuItem.Text = "CSV (coming soon...)"' + CSVToolStripMenuItem.Text = "CSV (coming soon...)" + ' + ' DarkModeToolStripMenuItem + ' + DarkModeToolStripMenuItem.AutoToolTip = True + DarkModeToolStripMenuItem.CheckOnClick = True + DarkModeToolStripMenuItem.Image = CType(resources.GetObject("DarkModeToolStripMenuItem.Image"), Image) + DarkModeToolStripMenuItem.Name = "DarkModeToolStripMenuItem" + DarkModeToolStripMenuItem.Size = New Size(143, 22) + DarkModeToolStripMenuItem.Text = "Dark mode" + ' ' FileMenuStrip ' FileMenuStrip.BackColor = Color.Transparent @@ -185,58 +205,63 @@ Partial Class StartForm FileMenuStrip.Name = "FileMenuStrip" FileMenuStrip.Size = New Size(355, 24) FileMenuStrip.TabIndex = 0 - FileMenuStrip.Text = "MenuStrip1"' + FileMenuStrip.Text = "MenuStrip1" + ' ' ToolsToolStripMenuTools ' - ToolsToolStripMenuTools.DropDownItems.AddRange(New ToolStripItem() {AboutToolStripMenuItem, DeveloperToolStripMenuItem, ChekUpdatesToolStripMenuItem, ToolStripSeparator2, QuitToolStripMenuItem}) + ToolsToolStripMenuTools.DropDownItems.AddRange(New ToolStripItem() {AboutToolStripMenuItem, DeveloperToolStripMenuItem, CheckUpdatesToolStripMenuItem, ToolStripSeparator2, QuitToolStripMenuItem}) ToolsToolStripMenuTools.Font = New Font("Segoe UI", 9F, FontStyle.Regular, GraphicsUnit.Point) ToolsToolStripMenuTools.ForeColor = Color.White ToolsToolStripMenuTools.Image = CType(resources.GetObject("ToolsToolStripMenuTools.Image"), Image) ToolsToolStripMenuTools.Name = "ToolsToolStripMenuTools" - ToolsToolStripMenuTools.Size = New Size(53, 20) - ToolsToolStripMenuTools.Text = "File"' + ToolsToolStripMenuTools.Size = New Size(28, 20) + ' ' AboutToolStripMenuItem ' AboutToolStripMenuItem.AutoToolTip = True AboutToolStripMenuItem.Image = CType(resources.GetObject("AboutToolStripMenuItem.Image"), Image) AboutToolStripMenuItem.Name = "AboutToolStripMenuItem" - AboutToolStripMenuItem.Size = New Size(152, 22) - AboutToolStripMenuItem.Text = "About"' + AboutToolStripMenuItem.Size = New Size(180, 22) + AboutToolStripMenuItem.Text = "About" + ' ' DeveloperToolStripMenuItem ' DeveloperToolStripMenuItem.AutoToolTip = True DeveloperToolStripMenuItem.Image = CType(resources.GetObject("DeveloperToolStripMenuItem.Image"), Image) DeveloperToolStripMenuItem.Name = "DeveloperToolStripMenuItem" - DeveloperToolStripMenuItem.Size = New Size(152, 22) - DeveloperToolStripMenuItem.Text = "Developer"' - ' ChekUpdatesToolStripMenuItem + DeveloperToolStripMenuItem.Size = New Size(180, 22) + DeveloperToolStripMenuItem.Text = "Developer" + ' + ' CheckUpdatesToolStripMenuItem + ' + CheckUpdatesToolStripMenuItem.AutoToolTip = True + CheckUpdatesToolStripMenuItem.Image = CType(resources.GetObject("CheckUpdatesToolStripMenuItem.Image"), Image) + CheckUpdatesToolStripMenuItem.Name = "CheckUpdatesToolStripMenuItem" + CheckUpdatesToolStripMenuItem.Size = New Size(180, 22) + CheckUpdatesToolStripMenuItem.Text = "Check updates" ' - ChekUpdatesToolStripMenuItem.AutoToolTip = True - ChekUpdatesToolStripMenuItem.Image = CType(resources.GetObject("ChekUpdatesToolStripMenuItem.Image"), Image) - ChekUpdatesToolStripMenuItem.Name = "ChekUpdatesToolStripMenuItem" - ChekUpdatesToolStripMenuItem.Size = New Size(152, 22) - ChekUpdatesToolStripMenuItem.Text = "Check updates"' ' ToolStripSeparator2 ' ToolStripSeparator2.Name = "ToolStripSeparator2" - ToolStripSeparator2.Size = New Size(149, 6) + ToolStripSeparator2.Size = New Size(177, 6) ' ' QuitToolStripMenuItem ' QuitToolStripMenuItem.AutoToolTip = True QuitToolStripMenuItem.Image = CType(resources.GetObject("QuitToolStripMenuItem.Image"), Image) QuitToolStripMenuItem.Name = "QuitToolStripMenuItem" - QuitToolStripMenuItem.Size = New Size(152, 22) - QuitToolStripMenuItem.Text = "Quit"' + QuitToolStripMenuItem.Size = New Size(180, 22) + QuitToolStripMenuItem.Text = "Quit" + ' ' LimitNumericUpDown ' LimitNumericUpDown.Location = New Point(89, 125) - LimitNumericUpDown.Minimum = New [Decimal](New Integer() {5, 0, 0, 0}) + LimitNumericUpDown.Minimum = New Decimal(New Integer() {5, 0, 0, 0}) LimitNumericUpDown.Name = "LimitNumericUpDown" LimitNumericUpDown.ReadOnly = True LimitNumericUpDown.Size = New Size(100, 23) LimitNumericUpDown.TabIndex = 15 - LimitNumericUpDown.Value = New [Decimal](New Integer() {5, 0, 0, 0}) + LimitNumericUpDown.Value = New Decimal(New Integer() {10, 0, 0, 0}) ' ' StartForm ' @@ -263,7 +288,7 @@ Partial Class StartForm MaximizeBox = False Name = "StartForm" StartPosition = FormStartPosition.CenterScreen - Text = "Reddit Post Scraping Tool" + Text = "ProgramName ProgramVersion" ContextMenuStrip1.ResumeLayout(False) FileMenuStrip.ResumeLayout(False) FileMenuStrip.PerformLayout() @@ -289,9 +314,11 @@ Partial Class StartForm Friend WithEvents DeveloperToolStripMenuItem As ToolStripMenuItem Friend WithEvents ToolStripSeparator2 As ToolStripSeparator Friend WithEvents QuitToolStripMenuItem As ToolStripMenuItem - Friend WithEvents SaveResultsJSONToolStripMenuItem As ToolStripMenuItem - Friend WithEvents ChekUpdatesToolStripMenuItem As ToolStripMenuItem + Friend WithEvents SaveResultsStripMenuItem As ToolStripMenuItem + Friend WithEvents CheckUpdatesToolStripMenuItem As ToolStripMenuItem Friend WithEvents JSONToolStripMenuItem As ToolStripMenuItem Friend WithEvents CSVToolStripMenuItem As ToolStripMenuItem Friend WithEvents LimitNumericUpDown As NumericUpDown + Friend WithEvents DarkModeToolStripMenuItem As ToolStripMenuItem + Friend WithEvents ToolTip1 As ToolTip End Class diff --git a/Reddit Post Scraping Tool/Reddit Post Scraping Tool/StartForm.resx b/RPST GUI/RPST/StartForm.resx similarity index 86% rename from Reddit Post Scraping Tool/Reddit Post Scraping Tool/StartForm.resx rename to RPST GUI/RPST/StartForm.resx index bc55380..491c3fe 100644 --- a/Reddit Post Scraping Tool/Reddit Post Scraping Tool/StartForm.resx +++ b/RPST GUI/RPST/StartForm.resx @@ -1,4 +1,64 @@ - + + + @@ -61,7 +121,7 @@ 132, 17 - + iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAYAAAD0eNT6AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO vAAADrwBlbxySQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACoYSURBVHhe7d0L @@ -574,6 +634,268 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJbvZS/7 /4YzHMz4V1N2AAAAAElFTkSuQmCC + + + + + iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAYAAAD0eNT6AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO + vAAADrwBlbxySQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAADvXSURBVHhe7Z0J + eFxV3f9vZlJ22WRRQERcAVEgkrY0KZO0FJJMOjNtbgoqiPq+BVFE/uJbUcG6VwSkCr70RQRBQEJn0lJA + 2ZW1ZNKKrCqLgKigiKxtaWbm/M+dnmI6c5Jmmcyce87n8zyfJ+jDkqRzft/vnbn3HA8AAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAQ8pnIhbl0dOVYLFxQd23+7Gj/WBRneYeobwEAAACqTS4T + vVOWADEmL4g8mz9bfh2D4lzvcPUtAAAAQLUZTwEoXBB5RRfuI5ECAAAAUEPGVQDOj+R14T4SKQAAAAA1 + ZFwfAZwv1YT7SKQAAAAA1BAKAAAAgINQAAAAAByEAgAAAOAgFAAAAAAHoQAAAAA4CAUAAADAQSgAAAAA + DkIBAAAAcBAKAAAAgINQAAAAAByEAgAAAOAgFAAAAAAHoQAAAAA4CAUAAADAQSgAAAAADkIBAAAAcBAK + AAAAgINQAAAAAByEAgAAAOAgFAAAAAAHoQAAAAA4CAUAAADAQSgAAAAADkIBAAAAcBAKAAAAgINQAAAA + AByEAgAAAOAgFAAAAAAHoQAAAAA4CAUAAADAQSgAAAAADkIBAAAAcBAKAAAAgINQAAAAAByEAgAAAOAg + FAAAAAAHoQAAAAA4CAUAAADAQSgAAAAADkIBAAAAcBAKAAAAgINQAAAAAByEAgAAAOAgFAAAAAAHoQAA + AAA4CAUAAADAQSgAAAAADkIBAAAAcBAKAAAAgINQAAAAAByEAgAAAOAgFAAAAAAHoQAAAAA4CAUAAADA + QSgAAAAADkIBAAAAcBAKAAAAgINQAAAAAByEAgAAAOAgFAAAAAAHoQAAAAA4CAUAAADAQSgAAAAADkIB + AAAAcBAKAAAAgINQAAAAAByEAgAAAOAgFAAAAAAHoQAAAAA4CAUAAADAQXLLoyvzK2QoXy/9lfRG6S3S + 26S/kf5Weof0TuXdynukaelVyiulVygvl14mvVT6M+lF0gulP5H+WHqeLAA/9GaqbwFgTLT7/tvaE/6B + 8WRXrD3ZPbcj6X+mPdn1tY5k9w/jSf8y+bVH/n/XS29WZqX90vs7Ev6j6q8D75Fu/Huu70h0/0L+sz+S + f883OpJdX4gnuo6LJ7o72+bMmzJ79tF7LFy4MKK+BQAAcxEPe1uIVd6HcvdFU/ls5FTpjwrZumvl1wel + r0rFmL1WqntnYGTmpc/kMtE75NefyzKxMNcbPV5kvMOlu6lvHxwmdvzxW3XOmXdIPOF3yVA+TQbx+cWA + TvoPS9dIRY1cL/2z9A5ZEC6X5eO7HanuT8jv9dDZs2e/RX37AADVQzzg7ST6vaZ8X+QUGdCX5bPRfvl1 + 7SahXUmDdw704V4JX5Tl4C75dXG+NzpfloImscLbRv2oYBlHJY/eJ7jKlkH/FRmqV8twfUQ6INUFsNG2 + J/1n5Neb5NfzisWgc+4Bvu9H1Y8KADA+xGPelqLPO0yG/Wnyin6Z/Pq8NqQn0uBjA314T5QDud5oVn49 + T5YDXyz39lC/DggRMgy3bp/dPb0Y9gn/OhmWLw4OUEt9tSPl/1aWgnM6Ul1HJxJHv0P9OgAAhkfc422d + y0bbZPAukl/vlF8n7sp+pFa/AOj8s/Ry6Yki7e2rfl1gEDLwt2tPdSVk8J0bT/j3yjAM3kLXhaRrPtaR + 6Pq/9oT/0SPnzHm7+nUBAMjQz3r75rPR+YVspEcG7itlAVxrzSgApT6RT0eWFN8h6PG2U79KqDLB297F + z+2T/q0y6N4oCT7U+0jwsUE82TWjYf78SepXCQAuIAMrOrCqvlWG64+lj28StiZqZgEY7OuF3rrr872R + z4jl3u7q1wwTQBBYbcmuNhliS6RPDwo1HJsvBfdCxFNdHzvS93dWv2YAsAkhvIi6cW+xDNW/lYWsyZpf + AAabL95U2Bs5hTJQGYIb29pT/hHxVPdFMrD+VRJgWDlz0pviia5PzvT9HdSvHwDCiljlTcn3R86TQfrX + smANi+EqAIMdKPTW/VoWgk+JG7zt1R8JjIy6zpR/eHui639lKP1jUEhhNUz4a+XXdLDvQfCYpPozAQDT + kVf6OwSf6cvgv18bqGEzvAVgsGsLmUjPQLp+phBenfqjghJmzz5m945E9wIZPo9tEkhYM2e2z/7N5KaW + CyY3z/iQ+mMCANOQV/sNMjCXSF/fJEDDrh0FYLCPSheIFd4u6o/OaYId72Toz1Q76nHnvkHK8H9Uhr8Y + ZL90fkNDJ3tlANQa8XtvWxmSn5P+YZPQtEn7CsBG10p/LjLeh9Ufp1MEnzO3p/wvyaAJdrvTBhDWzhlt + nc+VhP9gX5Iu+chhsQ+qP04AqBYy+HeT4bhQ+sImYWmj9haANw1uHMwti3a68PFA8Cx6PNm9UIbMv0tD + B81wxlGdr5YE/lAWpNdNboo1qT9eAJgoxErvvepO/jVlQWmrDhSAQf6x+ATBJZ51N161J/2DNhyew9v8 + Jtt6ZHygJORH6l1Tmlo75R8197gAVBJxn/fhQrZuuQzEQllA2q5bBWCjz0o/K3q8LdRLILTEU/5UGSzB + CXjawEFzbD2qI7ii14X7KGx9oLGp5bhYLFavXgIAMBbEKm8/GYKXSfObhKJLulkANvpM8R2BG7wt1Usi + NARH6Kob+wqlQYPm2XpkpybMx64sAY9Pbo593PM41hhgVIh7vX1k+AV39Oc2CUMXdbsAbPSp4mmFt3vG + X1UVt+cl+ENlpcO/xIcbm1p99fIAgKFQN/cFwT+wSQi6LAVgsI/mMtG4erkYRXDyXDzpXyEDJV8aMGiu + rUfFdaFdcRubWu7mZkEADaLfm6TO13+pLABdlwJQZiFTd4tY5h2oXj41pbOzcxu1ec+rpeGCZjvBV/46 + C5ObWq9pbJrxPvXyAXCbgWz9zHx/5GFt+CEFYGgHghMJRY+3q3opVZ14ortTBgnP8YfQGoT/YNdLF8di + MU7SrCTiRm9b0evto/4nGExwg18hW3ejNvTwP1IANucLsgicJBZ6VbvZqnPOvENkiNxdGioYDqv1tv8I + fJb7AyqIHATfkgPhDelisdx7i/q/wSCKb/f3RxbIcFtXFnZYLgVgZPZGVou0d4h6mU0Ivu9v3ZHqWiRD + JDg5ThsuaLY1vvLXOqWpdcWUWIwL1/EglnnvkIPg9UFD4a/FO4ereGUAwyOy3sEy1FaVhRwOLQVgNK6X + LpqIxwbbZ3dPjyf9P+hCBcOhieE/yNcnN7V+uaGhYZJ6ycFoCE4bKxkGRXO90awsB4epvw1qgLzq30aG + 2SIpj/WNVgrA6E1HHhtI17eol9+4SCaTO8rwWCLlsb4Qa3j4D7L1gUMPix2kXn4wEkTamyYXfqFsEPzH + QlAQRI+3t/pHoEoMrKpvlUH2VFmw4cikAIzVvCwCP5Zrfmv1Uhw17amuhAyP50rDBMOlQZ/5j9S1U5pi + C3zfj6qXIgxF8BZ/PhPt1wwAna/LobDQxr3GTSPYtEUGWHBgD1f945ECMF4fFUu9g9XLckQEn/XHE12L + ZXhw1R9yw3Plr7E5ds/k6dPfq16WoEMG+gmaRb85n8llosepfwVUGNHvfUCGF5/1V0IKQCVcXyz+I7gf + aMMWvv4DpUGC4TPU4f8fX5/cFDtFvjQ5ZKgUcYO3vVzcfy9Z7CM2l6m73dXzyCeKXDZ6nAyu18qCDMcm + BaBiFjcQ6vH2VC/VUuriSf8UGRxvlAYJhk9Lwn+Qrb0HxWI7qtcqBMhFfU7pIh+D+Xxv5DJZBHZT/1oY + A/Kqf4dCtq5XG2I4dikAlfafud5om3rZFpmVSu0mw/9WXZBg+AzhZ/4j9ampza2N6mXrNmK59x65mNeV + LO7x+G/pAhuOIK026i3/R8rCC8cvBWAiDG4YXhR8JCADo0H61OAAwfBq35X/pjY2taxTHwm4TaG37vqS + RV0p/5hLRzvUfwY2Q64vOk8GFW/5T5QUgAkz863339+R8NfqggTDp+3hv4nTWi//0KxZ26ox7BYDmfoj + dAu6ksqCcbNY6h2g/pNQgujxojKggmf7C5sEFlZWCkDFXXdNvVj8pUO1IYLh1Knwf9PWB6ZOjb1HjWQ3 + KD5elok8VLqoJ8hgZ7HFMux2UP95kIj7vLfmsnW3awMLKysFoKI+/4ttxRc+PVMbIhhOLf7MfyT+a0pT + LKZGs/3IRXxq6aKugi/keyOnBFe96ttwFpH19pXB9IeyoMKJkQJQMZ+4eCfxiY91aEMEw6mbV/5lvtHY + 1GL/Y+0ygHeWC/lfpQu7am44cGS6+nacQ6zypuT7Is9rgwonRgpARVx1wduE353UhgiGU8K/zMWet9De + s2/kQr6wdGHXwkKmboW41nuX+racIJeNzpWBtKYsoHBipQCM219/f1+RSM3VhgiGU8J/KFuvmTp16pi3 + vzaW4IY8uZgHShd3DV0jXeTCscPyqv8UGUb5snDCiZcCMGZz6Yj4xdc/qA0QDK+E//A2NrXcPXXqkTur + 8W0Hwa59ukVugM8G2woLYd9WjcHPlO+PnKcNJqyOFIAxuX5pVJz9/yZrAwTDq+M3/I3C1kemTTtiDzXK + w00uHe3SLXKTlCWgT2S8qepbDj0y/CPyyv8ibShh9aQAjNo3ltaL75xymDZAMLxy5T9qnzy0aea+aqSH + k+Dkvnw68qRuoRtoobitcI/3NvXth5LiM/59kUu1gYTVlQIwKoNn/L95cpM2QDC8Ev5jc0pTy9OhPlFQ + BurXdAvdaNOR18J67LB42NuikK1bqg0jrL4UgBH7+tVbiK985nBtgGB4JfzH7XOTm2d8SI348CCvRPeU + C/vV0oUeIh/PZaK++nGMR9zjbS3D/1faIMLaSAEYka9cvaU4bX6rNkAwvPKZf4VsbnlBfm1Qoz4cyIV9 + eelCD6OFTN2tYqlndAMT/d6kQl/dCm0IYe2kAGzWNT2TxIITYtoAwfDKlX/F/ffU6dMPViPfbETamyIX + d3Bql3bRh9ANxw73eLuqH9EYip/5ZyO/LAsfrL0UgGENbvg746Tp2gDB8Er4T5jPT50a+4Aa/WYSPH6W + S0dX6ha8Bb4oNebY4eKjftnIT8uCB82QAjCkb6TrxUJu+LNOwn/C/cuUWGwfFQHmkctEP6Fb8Jb5h9zS + aLv6kWtCMfz7Ij/RBg+aIQVA60A6KhZ9Yao2QDC8Ev5VsrnlsYbm5rerKDALWQDu0C16Gy1k6paJ5V5N + jnTM90d+oA0dNEcKQJnBDn/nntaoDRAMr9zwV12nNLU82Ng4460qDsxBLPEmBafvycX+cunit9QNxw7f + 4G2vfgUTjgyXE8rCBs2TAlDmZWceqA0QDK9c+dfMlcaeHSDS3tvz6cgSuehzpUPAUv8ZFJ/gpjz1K5gQ + cv3RDhkuubKwQfOkAGziTWe9SxsgGF4J/xo7rWWp0acIil6vIZeJ3qkbCJa6SizzmtWPX1HEKq9BBstr + ZUGDZkoBeNP7f7KbSMzhVD+bJPwNsbnluyoizKT4ZEAm6stB8FTpYLDV4rHDvV7F7tYU93r7yFD5e1nI + oLlSAIr++Wc7im7O87dKwt8sG6fFTlBRYS5ihbeNHAgLpGHeIXA0vi5dJHq87dSvYEyIfm8HGSgPlQUM + mi0FQLxw5Tbikx/r0IYIhlNu+DPS9Yc2t8xQkWE2Iu3tFWysIweETZsFDeeYjx0O/plCf11GGzBoto4X + gOBZ//85oUUbIhhOufI32heN3yhoMGKZN9niTYPKlCXgvmCXRPXjjwgZJAvLggXDoeMF4CcLDtGGCIZT + wj8U/nHy5LaqPZE2bsRCLxJcHcuB8VzpALHUDdsKL/d2V7+CIRnI1h8hg4Q7/sOqwwXgtnPeqQ0RDKeE + f3hsbGq5WkVIeAg+Jw+O4ZXDY13pMLHSjccO3+BtqX4Fm6Bu+nuhLFQwPDpaAB6/eGcxp2uONkgwfBL+ + 4XNKc+vJKkrChVjqvbeQifToBouVpiOPBU9IqB+/SHC0rwyQVWWBguHSwQLw8lVbiU9/vF0bJBg+Cf/Q + un5Kc8s0FSnhY6C3foYcKA+WDhhbLWTqbhEZ74PBzy7D42dlYYLh07ECEGzzu/BzzdogwfDJ3f6h95mG + WGyXYqCGEXG7V5/vjc6Xw+WfpcPGUgdyN0Vv0oYJhk/HCsC133uvNkgwfHLlb4uxG4zeKXAkiB5vZzlg + Fkvt3lZ4ufQ+qS5MMHw6VACeumRHPve3RMLfLhubW05VURpuRMbbr9Bb92vdAAq9vdI7pbogwXDqSAFY + d029OPlTR2jDBMMl4W+fjU0t6z5yWKz48bIV5JZFO+XgeaJ0EIXaW6S6EMHw6kgBuOgrB2nDBMMl4W+1 + qxoaGiapCA0/osfbwppjh6+T9kl1IYLh1YEC8LsLdhfxZJc2UDA8csOf/TY2xb6u4tMexHJvD3XscL50 + OIXGu6W6AMFwa3kBWNMziUf+LJArf2ccmNrc2qii0y7EUu8juUz0Lt2gMlre+rdXywvAktMP1gYKhkfC + 3zkfnTp16tYqNu3izWOHeyNP6waWcV4r5a1/e7W4APzhol1EZ4q3/sMs4e+qse+pyLST4rHDG7YVXls6 + uIySu/7t1tICsH5pVHyeu/5DLeHvtG9MmT59PxWX9iKWee9Qxw5rB1lNvUmqCw20R0sLwJULD9CGCoZD + wh8nT2v5rYzIUR9PH0oGer2YHFz3lw6ymsmGP25oYQF45tIdRHLuXG2woPlytz9udMq02MdURNrPoGOH + ny8dalX3N1JdYKBdWlgAzvwse/2HVa78scTnDorFdlQR6Qai19tRDrJF0jcGD7aqyTP/7mhZAVh9wdu0 + wYLmS/ijzinNsR+raHQLkfHeV8jUrdANugn1LqkuLNA+LSoAwY1/Jx0/SxsuaLaEPw5jbkpz6yEqFt1j + IF0/Uw64h0oH3oR4o1QXFGinFhWAZd95nzZc0GwJfxyBt6s4dBOxxJukthV+qXTwVczgsJ97pbqgQDu1 + pAC89MutxNHzEtqAQXMl/HHEHtY6S8Whu4iMt5sceP8nrfy2wrdKdSGB9mpJAfjfLx+iDRg0V+72x1F6 + v+ctjKgodBux1Ds4l4n+VjcMx+QyKY/9uacFBeC5y7fjsb+QyZU/jsXGppaPqgiEgOKxw+nIk7rBOCq5 + +ndTCwrA4i99RBsyaKaEP47ZaS1/bmtr21LFHwSIHm9rOQgXSF8ZPBhHbHD1z2N/bhryAvC3y94iEnO4 + +g+LhD+O39gpKvpgMMVjhzdsK1woHZTDytW/u4a8AJz9xcnaoEHzJPyxQj7f0NC5jYo9KEWkvUNzmeg9 + uoFZZrDlL1f/7hriAhBs+Tt7Dqf9hUHCHyvplObWk1XcgQ517HCwrfDfSwfnJt4m1QUDumGIC8D3T52i + DRs0S+72xwnwmQMO8LdQcQdDIW70th3y2GE++8eQFoDgs3+u/s2XK3+cKBubWo5TMQebQ3vs8C1SXSig + O4a0AFx4+sHawEFzJPxxgn2UfQFGyUC6vkUO0N8Xd/1bKdWFArpjCAvAy1dtJeZ2pbShg2ZI+GNVbG5J + qGiDkVI8dvg30Yu1gYBuGcICcPU399OGDpoh4Y9V9D4VazBSghsE5fB/qCwM0D1DVgDeSNeLT3w0rg0e + rL2EP1bfWJOKNhgJuWy0TRsG6J4hKwC3/GAfbfBg7eVuf6yJ01ovV9EGI6GQrbtOGwboniErAKf+1wxt + +GBt5cofa2VjU8u6ww6btZuKNxgOsdLbSw7+XFkQoJuGqAA8cfFO2vDB2kr4Y61tbG75koo4GA459L9e + FgLoriEqAD/hyF/jJPzRBBubWh7nkcDNIIQXkUP/qbIQQHcNSQFYe80kMW9eQhtCWBsJfzTKaTOOUFEH + Orj5D8sMSQHg5j+zJPzRQNMq6kBHob8uow0BdNeQFIAvzW/RBhFWX8IfTXR665EPzp59zO4q7mAwYrW3 + qxz468sCAN02BAXgLz/fXhtEWH151A9NtLnlyBfbE/7aeNL/nIo8GEy+P3KiNgDQbUNQAK5ceIA2jLC6 + cuWPJtrcOmtAhv869Tq9W0UeDKaQrbtNGwDotiEoACcdP6ssjLC6Ev5oojL8RXvSXz/otVro7PTfpWIP + AkSf9zY57Hn2H8s1vAA8fekOmwQRVl/CH020GP6JroHS12s82XW6ij4IkIP+c2WDHzHQ8AJw+Zkf3GRx + Y3Ul/NFE1ZV/XvealT6sog8Ccn3RO7TDH9HwAnDiJ47ULXCsgoQ/mmgQ/h2JroLuNbvRzrlzP6Diz23E + Pd6ectDnywY/YqDBBYCtf2snd/ujiaq3/bWv2cG2p3y2Bg6QQ/6EsqGPuFGDC8CVC/fXLm6cWLnyRxMd + afgr71AR6DaFbN0y7eBHDDS4AJw2v1W3sHECJfzRREcZ/oG5zs5jdlEx6CbiYW8LOeRfKRv6iBs1tAC8 + fNVWYvacUS14HKeEP5roGMK/aHvSP1ZFoZsMrKpv1Q59xI0aWgB+e+7e2kWNEyPhjyY61vDfYHePikI3 + yfdHfqAd+ogbNbQAnPPFRs2CxomQ8EcTDcK/Y8zhX/Tfvu9HVRy6hxzwD5UNfMTBGlgAcumIOPaYTt2C + xgpL+KOJju/KfxMbVBy6hVjp7aUd+IiDNbAAPPbTnXULGSss4Y8mWsHwl3Z9UUWiW+Sy0WO0Ax9xsAYW + gGXffZ9mIWMlJfzRRCsb/tKEf52KRLeQw/38smGPWKqBBeB7X5iqX8xYEQl/NNGKh/8GX4nFYvUqFt0h + 3x+5XzvwEQdrYAH4xEfjuoWMFZDwRxOdoPAvGk/6jSoW3UCs9LaXw53T/3DzGlYA/nbZW7SLGMcv4Y8m + GoT/OO/2H95E92kqGt1goL9+lnbYI5ZqWAG47Zx36hcxjkvCH010Iq/8/2PX1Soa3SDfH/mGdtgjlmpY + ATh/QYNmAeN4JPzRRKsT/kWfVNHoBoVs3Y3aYY9YqmEF4POfOkK3gHGMEv5oolUM/6Jtvr+rikf7kYP9 + ubJBj6jToAKwfmlUJOfO1S5gHL2EP5potcM/sD3lH6Xi0W7Eam9X7aBH1GlQAXjyZztpFy+OXsIfTbQW + 4b/B7jNURNrNQLb+CO2gR9RpUAHgBsDKSPijiQbhP6F3+w9je9LvVRFpN/n+yBe1gx5Rp0EF4JKvfUi7 + eHHkEv5oorW78n/TP6qItJt8X+RS7aBH1GlQATjzs826hYsjlPBHEzUg/AMH2tratlQxaS9yqK8uG/KI + Q2lQAWAHwLFL+KOJGhL+RTs75x6gYtJOhPAicqivLRvyiENpSAF45eottYsWNy/hjyZqUvgHxhN+l4pK + OxH3eHtqhzziUBpSADgCeGwS/miipoX/Bi1/EkD0e03aIY84lIYUgLvP20uzYHE4CX800SD8a3W3/3DG + k/4VKirtJJeNHqsd8ohDaUgByHz7/dpFi3oJfzRRM6/83/RuFZV2ku+PnKEd8ohDaUgB+N8vH6JbsKiR + 8EcTNTz8g70AnlFRaSdyoF9cNuARh9OQArDw5CbtosVNJfzRRE0Pf2UuFovVq7i0j0K27jbtkEccSkMK + wEnHz9ItWBwk4Y8mGpLwL9rZ6e+t4tI+5EB/vGzAIw6nIQXA705qFyxukPBHEw1T+Ae2zemepuLSPuRA + f7VswCMOpwEFYO3SSdrFihsk/NFEwxb+RVNdR6u4tAvxmLeldsAjDqcBBeD5X2yrX6xI+KORhjL8i3Z9 + QUWmXYjV3h7aAY84nAYUgMcvZhMgnYQ/mmh4w1+a8L+hItMuxCrvQ9oBjzicBhSA312wu36xOizhjyYa + 6vDf4I9VZNrFwKr6Vu2ARxxOAwrAb8/dW7dQnZXwRxO1IPxFR6L7Fyoy7SLXH/W1Ax5xOA0oACu+9x79 + YnVQwh9N1Irwl8aT/g0qMu1CDvMTyoY74uY0oABcuXB/7WJ1TcIfTdSW8A+MJ/x7VWTahRzmp5YNd8TN + aUABuOSMD2kXq0sS/miiQfibeLDPmE34j6rItIt8f+TL2gGPOJwGFICLvnKQfrE6IuGPJmrTlf8gn1SR + aRf5vsiZ2gGPOJwGFACXDwIi/NFELQ1/0ZHyn1WRaRdymH+7bLgjbk4DCsCP/ucj+sVquYQ/mqi14b/B + f6jItAs5zM8qG+6Im9OAAnDuaY26hWq1hD+aqOXhH/iSiky7yPdHztMOeMThNKAAnHXqFN1CtVbCH03U + gfAPXKMi0y7yfZGfaAc84nAaUAC+c8phuoVqpYQ/mqgj4R+YU5FpFxQAHJMUgKpJ+KOJOhT+gZYWAD4C + wLFoQAH4vgMfARD+aKKOhX/g6yoy7UIOc24CxNFrQAE454t23wRI+KOJOhj+gS+qyLQLOcx5DBBHrwEF + YPGXDtUtVCsk/NFEHQ3/wOdUZNoFGwHhmDSgAFywoEG3UEMv4Y8m6nD4Bz6tItMu2AoYx6QBBWDJ6Qfr + FmqoJfzRRB0P/8DHVGTahRzmHAaEo9eAAvCzr9l1GBDhjyZK+EtT/kMqMu1CDnOOA8bRa0ABuPob++kX + awgl/NFEg/C36lS/Mdqe9PtUZNpFLhvt0g54xOE0oABcv+jd2sUaNgl/NFGu/P9jPOnfoCLTLgayXkw7 + 4BGH04AC8Ntz99Yu1jBJ+KOJEv6ldl2uItMuRL93oHbAIw6nAQVg9QVv0yzU8Ej4o4kS/jq7f6gi0y5k + AXi7dsAjDqcBBeCPF71Vs1DDIeGPJkr4621Pdn1NRaZdiIe9LeRAL5QNeMThNKAA/O3nb9EuVtMl/NFE + Cf9h/YyKTPuQA/3lsgGPOJwGFIA1PZN0C9VoCX80UcJ/M6a6fBWX9iEH+uNlAx5xOA0oAIFHz0voF6yB + Ev5ookH486jf8MaTXTEVl/ZR6Ku7VTvkEYfSkAJw8qeO0C5Y0yT80US58h+ZnZ3+u1Rc2occ6BeXDXjE + 4TSkAHzj5CbtgjVJwh9NlPAfsQMN8+dPUnFpH/n+yBnaIY84lIYUgJ8sOES3YI2R8EcTJfxH5Z9VVNpJ + ri/6ce2QRxxKQwpAzzfN3Q6Y8EcTJfxHacq/TUWlnYisN0075BGH0pACYOpugIQ/mijhPxa7LlZRaSfi + Hm9P7ZBHHEpDCsDjF++sWbC1lfBHEyX8x2r3GSoq7UQILyKH+pqyIY84lIYUgLVLJ4nOlDlDjfBHEw3C + n0f9xmr3x1VU2ks+G+3XDnpEnYYUgMBPf7xds2irL+GPJsqV//iMp+Z9WMWkvcih/rOyIY84lAYVgK9/ + tlm7cKsp4Y8mSviP24G2trYtVUzaixzqp5YNecShNKgA/PSrH9Yt3KpJ+KOJEv4V8UEVkXYz0Fc/Qzvo + EXUaVABuOutduoVbFQl/NFHCvzLGk/4VKiLtRqz2dtUOekSdBhWAJy7eSbt4J1rCH02U8K+c7Sn/yyoi + 7UcO9r+XDXpEnQYVgIF0VMz1U9oFPFES/miihH+FneN3qHi0n0Jf3a+1wx6xVIMKQOBp81v1C3gCJPzR + RIPw51G/yto2Z85eKh7tRw72r5cNekSdhhWAC08/WLuAKy3hjybKlf+E+BcVjW4wkK2fqR32iKUaVgBu + O+edugVcUQl/NFHCf6LsulJFoxuIh73t5HAfKBv2iKUaVgCevmQHzQKunIQ/mijhP3G2p7o/q6LRHeRw + X1027BFLNawA5NIR8bFjOrULebwS/miihP/E6sQOgKXI4f7jsmGPWKphBSDwe1+Yql3I45HwRxMl/Cfc + l33fj6pYdIdcNnq0duAjDtbAAnD9ovfoFvKYJfzRRAn/qvhrFYluoY4GLpQNfMTBGlgAKnkfAOGPJhqE + P4/6VcGU/1UVie4hB/yDZQMfcbAGFoDA4z4a1y/oUUj4o4ly5V9VG1Qcuocc8GeVDXzEwRpaAM46dYpu + MY9Ywh9NlPCvqv9YuHBhRMWhewz017dohz7iRg0tALeevY9uQY9Iwh9NlPCvru2p7p+rKHQT0e9NkkP+ + pbKhj7hRQwvAS7/cSsyeM/phSfijiRL+1bc92X2MikJ3KWTrerWDHzHQ0AIQuOCEmHZhDyXhjyZK+NfE + fJvv76pi0F3y2eh87eBHDDS4ACz91gd0C1sr4Y8mSvjXxnjCv1dFoNuI1d4ectDnywY/YqDBBeDZS7fX + Lu5SCX800SD8edSvRia6v6IiEHLZ6G+0wx/R4AIQeMInjtIvcCXhjybKlX9tnT3bf4+KP8j3RU7SDn9E + wwvAZWceqF3ggTOOimuHL2ItJfxrblZFHwSI1d6ucthzOiCWa3gBePpS/a6AR7Z3aIcvYi0l/A0w0X2a + ij7YSCFbd4s2ANBtDS8AgSd/6ohNFvjs2W3a4YtYSwl/IywclTx6HxV7sBGeBkCtISgA13zzP08DdM+d + pR2+iLWU8DdD7v4fAtHv7SIH/vqyAEC3DUEBeP7y7UQ82SU+ecwM7fBFrKVB+HO3vyl2fUFFHpRSyNal + tSGA7hqCAhD4nc8drB2+iLWUK3+jfGNWKrWbijsoZaCv/ihtCKC7hqQAXPftt2sHMGKtJPxNs7tHRR3o + EMKLyKH/VFkIoLuGpACsu6ZeHHlEk3YQI1Zbwt8821P+ESrqYCjyfZEztUGAbhqSAhD4w1Peqx3GiNWU + 8DfSJ50++nekiJXeXnLw58qCAN00RAXg6Yu3FVOa9UMZsRoS/oaa8r+qIg42R6GvboU2DNA9Q1QAAk86 + hpsBsTYS/sY6EI/7e6p4g82Ry0bbtGGA7hmyAnDz93bXDmfEiTQIfx71M9N40l+qog1GSr4/cr82ENAt + Q1YA3lgaFe2zpmmHNOJEyJW/2cZT/lQVazASxO1eff726KXaQEC3DFkBCPzZl/fRDmrESkv4G++dKtZg + JAxk6lvlEH0g3yuH6UqpLhTQHUNYAF7+5RaidUazdmAjVkrC33w7k91JFW0wHGK5955CJtKzyTC9RaoL + BXTHEBaAwB+f+h7t0EashIR/KPwjj/5tBnGjt20+HVkoh+a60iGaXya9T6oLBnTDkBaAf16+lZgeO1w7 + vBHHI+EfDuWf0Qkq5qAUIby6XCZ6nByWfy8dnpt4m1QXDOiGIS0AgYs++37tAEccq0H4c7d/KHzO9/2t + VdzBYESv1yjD/17d0CxzubRPqgsHtN8QF4C/XLKNOGx6TDvIEUcrV/7hMZ70T1FxBxsRPd6e+d7IZXI4 + FkqH5bDyLoC7hrgABJ7x3wdohzniaCT8Q+XfuPofhAz+reUwXCB9dfBwHLHBvQC8C+CmIS8AwbsATYdz + LwCOXcI/dH5GRR/klkU75SD8c+lgHLW3SnUBgXYb8gIQeNbnuBcAxybhHzqfklf/W6j4cxeR9g7JZaJ3 + 6AbimOSJADe1oAD864otRUvrdO2ARxxKwj+Epro/pSLQTcRyb/d8OvJTOfjypYNw3LIvgHtaUAACl5y2 + r3bII+oMwp+7/UPnH2OxWL2KQrcQS7xJ+d7IKXLYvVQ6/ComuwO6pyUF4LWrJ4m2IzkjADcvV/7htD3V + FVdx6BYD6fqZcsg9XDr0JsRfS3VBgXZqSQEI/OUZ79AOfMSNEv6h9RYVh+4glnvvL/TWXa8bdhPqXVJd + WKB9WlQA1l1TL7riU7SDH5HwD60DHal5H1SxaD/iOm8nOdAWSd8YPOCq5nVSHgt0Q4sKQODKH75VO/zR + bQn/8BpPdC1W0Wg3YqEXUdv3Pl862Krub6S6wEC7tKwABJ7+6Q9qQwDdlPAPtf9KpVJvVRFpLwPp+hY5 + vH5fOsxqZrBFMI8F2q+FBeAfv9hKzGjluGDcEP7c7R9q7d70Ryzz3qG279UOs5p6o1QXGmiPFhaAwCu+ + trc2ENAdufIPt/GEf6+1x/0OOqZ3benwMkpuCLRbSwvAQDoqjp1zqDYY0H4J/9D7RlvC31/FpT2oY3p9 + edX/tG5wGee1Um4ItFdLC0Dg/efvJKY2c1qgaxL+Fpjyz1SRaQ8i7R2aS0fv1g0ro71ZqgsPDL8WF4DA + cz//Xm1IoJ0S/haY8B9ta2vbUsVm+BHLvT3y6cgSOZAqv31vtbxbqgsQDLeWF4Bgb4CPJhu1YYF2Sfhb + Yb5tTvc0FZ3hRvR4W6jte18pHUyh8zopHwXYp+UFIPCRC3cQ06bzUYDNBuHP3f7htz3pn6PiM9wUj+lN + R57UDaTQykcB9ulAAQjksCB75crfGh+MHX/8VipCw4no9Q7KZaK/0Q2h0BscFsRTAXbpSAFYvzQqPtnV + oA0QDK+EvyUm/LXtCf9AFaPhQ/R4O8tBs1iaGzx4rJMNguzSkQIQ+MRPtxPTY4drgwTDJ+FvkYmuk1SU + hotBx/T+u3TgWOpA4abojdowwfDpUAEIvObre2nDBMMl4W+P8aR/g4zSug2JGiLUMb0PlQ4ZWy1k6m4R + Ga94KpMMj5+WhQmGT8cKQODC+ftrQwXDITf8WWTC//usVGq3YqCGBRmC75NhuEI3XCz1T8HmRerHLyJu + 97bKZ6P92lDB8OhgAXjt6kni6ASPBoZRrvytcqAz5R+uIsV8RK+3oxwgwTG96wYPFIt9NdiuWNzgaTdl + EKu9d8oQ+WdZqGB4dLAABP754u1E6wwODAqThL9tdn1BRYnZGHVMb3XMBwcUieXe7upXMCQD2fqZMkhy + ZcGC4dDRAhB40/d21wYNmifhb5mJ7qtUhJiNyHiHy2Fxf+nwsNVcOrpSLPMmqx9/ROT7I2dowwXN1+EC + EPj9z75fGzhojoS/dT4wa9ax26r4MBOR9vYKroLlkCiUDg1L/UvwLkdwWJH6FYyY4J8pZOvS2oBBs3W8 + AARbBZ8472Bt8GDtJfyt85XOuXM/oKLDPMQKb5tQHNNbOV+XLhI93nbqVzAmRL+3gwyUB8sCBs3W8QIQ + +O8rtxR+52RtAGHtDMKfu/2tcqAt2dWmIsMs3jymNxN5qnRA2GrwJINIe+9Uv4JxI+7x9pSh8peykEFz + pQAUfeaSbcWRRzRpgwirL1f+9tme6v6sigqzEL1egwz/u3SDwVJXiYzXpH78iiL6vUNksLxaFjRophSA + N131o51F0+HsFFhrCX8LTXUtUhFhDvLq9+2hP6Z3dP4z2LVQ9HhR9SuYEHLZaLsMl4GysEHzpABs4o3f + 211MadYHE068hL+NdvcsXLgwouKh9gw6pvfl0gFgqeuli8UN3vbqVzDh5LPR+drAQbOkAJR5IScH1kTC + 3z7bk35fZ2fnNioWzCCXid6hW/g2WkjX9Ype793qR68qMmC+XxY4aJYUgDJz6YhYxOOBVZUb/qz0kc7O + Y3ZRcWAOud7ox3UL3zL/IH/Omt9xKUPm3LLQQXOkAGgNSsC3P7OfNqywsnLlb6VPd3b6e6sYMAt1x/+d + uoVvgS8WP+e/3atXP25NCX7X+b7IRdrwwdpLARjSgXRUnP7pD2pDCysj4W+hKf/Zjrlz91URYCYi7R0i + F7lNN/8NBDc0ih5vV/UjGkNw06EMm6vKwgdrLwVgWIONgk499sPa8MLxSfhb6T/aEv7+avSbjVzgl5Yu + +DBayNTdKpZ5B6ofy0hEvzep0Fe3QhtCWDspAJt1TU+9OPEYdguspIS/lb7UOWfeIWrkm09w2I1c4OF9 + EiAdeSzYvEj9OMYTHCFcyNbdoA0irI0UgBH5ylVbiBOOpgRUQm74s9J/xZN+oxr14SHfGzldt+CNNh15 + LdiuWFzibaV+jNAgHva2kCXgGm0YYfWlAIzY16+eJD7/sYO0oYYjkyt/+5TB/3w8Ne/DasSHi+KeAJnI + n0oXu6EWggOK5Pf8NvXthxJ1T8DPysIIqy8FYFQG9wQs+OSB2nDD4SX8rfRvnZ1zD1CjPZzk0tGUbrGb + ZC4T7RMZb6r6lkOPejpgsTaUsHpSAEZt8HTAN07YXxtyqJfwt9I/tyXn1WSPmYpTyNTdqFvsBvisDP8x + HdNrOqoEnK0NJqyOFIAxGewTcPbJ79OGHW4qn/lb6cOzZx+9hxrl4Ucs8/aXC3ugdKHX0DXScR/TGwZk + CThFhlG+LJxw4qUAjNmgBFzwxXdrQw83yJW/ld7d5vvGPW4+bvK9kQt0C73aFo/p7fX2Ud+WE+Tui6Zk + IL1eFlA4sVIAxu2139qDUwQ1Ev422t0TO/740N18PiLk1fbOckG/ULrAq2ZvZLVIe9PVt+McIus1ylB6 + riykcOKkAFTE352/kzjyiCZtELoo4W+f8UTXYqNO9ZsI5GL+fOniroIvFLfvneBjesOAuM97lwymR8qC + CidGCkDFfPribYXfOVkbiC5J+FvnQDzVfaIa0XYT7KEvF/ODpYt7gtxwTG+Pt4P6z4NE3OPtXMjW3awN + LKysFICK+uIVW4oT57m7YRA3/FnnCx2J7plqNLvBQG/9DN3irqSF3rqbgxsP1X8SSlB7BSyUcnPgREoB + qLhrr6kX3zrRvZMEufK3zt91dvrvUiPZLQrpumt1i7sC/jGXjnao/wxshlxftFMG1UtlwYWVkQIwYV5+ + +jtXT2mKrdGFpW0S/rbZdXlnZ+c2agy7h+j13i0X8brSRT0OX5QukFe2W6j/BIwQcZ/3PhlWD5WFF45f + CsBEmCtu1b3Qi0yZPn2/yU2tj+hC0xYJf6tcF0/6p6jR6zZyIf+gZGGPxXxx+96Mt5v618IYEHd5bylk + Iz3aEMOxSwGotM8PZOqPUC/bItOmTXtLY1PL1brwDLt85m+VT7fNmTdFvWxBLPfeIhf030oW+IgtZOpu + E0u9D6l/HVSAXDZ6nAyuV8uCDMcmBaBiyvV+k5wZQ+2OVtfY3HKqDM31pSEaVrnyt8iUf82Rvr+zeq3C + RnLp6H/pFvtmfCbYvlf9K6DCBI8KyiJwlzbQcHRSACrhWumC4C1/9RIdkqnTpx8ow/N3pWEaNgl/a3xF + Ol+9PKGUYFEHB/GULHi9IT6mN2wUH9fc8JRAbpNAw9FJARivD4uMN6qjUGOx2FaNTa2LZJDmSoM1DBL+ + 1njf7Nn+e9TLEoZCLPMOkwu9ULLwB1soZCI9osfbW/0jUCVEnzddBtkTZcGGI5MCMFbz0nPHU/YbD5sx + dXJzy2O6kDVVPvO3wvXxZPdCWUTr1UsRNke+N/LLkgFQNNcbzQYFQf1tUAPEPd7WMswWSXk3YLRSAMbi + gyLtVeRmqYaGzm1ksC6WFgYHrYly5R9+40l/deeceYeolx+MFLng9yq+xf+fIfDXfG90/kg+94PqIPq8 + g3LZaFYbdKiXAjAag507F4kbvC3VS65iNDa3HGnyuwGEf+h9vSPRvcD3fee3mx8zwef7cgC8IV0cPCGg + /m8wiOK9ARuOF36tLOywXArAiMylo3dP9M6dDQ0Nk6Y0xRbIwF1bGsC1lPAPvbfH5859r3qZwVgRK7xt + XDumN6yIld57C9m667Whh/+RArA5/5HLRP+7mu/0Hdo0c9/JzS3X68K42vKZf6j9S3uy+xj1sgJwj4G+ + +hky6B4sCz7cIAVgKDcc1NXr7aheSlVnSlNrpwzhp0pDuVpy5R9a13SkuhbNnj2bd6kBNjwyGJ0vA++f + ZQHouhSAMosHdS31DlAvn5oSi8W2m9zccqYM5FdKA3oiJfzDaTzprzgqeTTvUgOUIvq9XfL9kQtk8K0v + C0JXpQAM9qGBTP1R6uViFI2NM94a7B3Q2NSyThfYlZS3/cNne9Lv60z5TerlAgBDIVZ775Tht0Q6sEkY + uigFINjA68niEz09nvF3SDc0zdxbhvQSaX5waFdKrvxDZsp/qCPV5cuXRt2GVwgAjIhgS2EZgkERcHf/ + ALcLwFPF4L/dC92GKFOaWw9pbGr5lS7ExyrhHyofiSe6juOxPoBxIrLeBwvZurQMxHxZQNquiwWgN/K0 + vOo/QSzxJqmXQGiRJeDDMrwvk45rW2He9g+Nf+pIdn+c4AeoMKLfe3e+L7JYBuPrZUFpq24VgPuDQ7ps + CP5Sio8ObthRcNR7CKgr/4ImbNAc+4MrfrbvBZhgxGpvVxmOwUFD9j814EABkKF/V25ZtFMI+z8nnRyL + 7TVlWsu5MthfLQ16nRvC389pAgdrbz64q78j0T1T/fECQLUQ/d42+f7IZ2RQPlIWnLZobwFYk09HLhHL + vAPVH6dTTJ7ctr0M+PnS+wcH/mBl+Oc6El0DmuDB2hoc0fsTdu8DMASxymuQgRncMGjXFsP2FYBHpAtE + xnur+qNzHhn2DdLgyYE1g8L/3+1Jf11J8GBt7ZfO931/O/VHBwAmIVZ626tNhVaXhWkYtaMArAmO4x5I + 1/NW6TA0Nzfv2tjc8qXprUfeJq/8CX8zfLEj2f2j9oTv5DtVAKFF3Od9JN8XOVsG6TNlwRoWw1sA3ihk + 6q4r3tTX43HFNEraE3Mntyf982QA/a0kkHCiTfhrO1J+Jp7w58WOP34r9UcCAGFF9HkHyEBdJP3rJgFr + uuEqALnghr58b+QUkfF2U796GAfB42TxZNeMjmTXxTKc5NWoJrCwEq6ThevaeKrrY+zRD2ApQngRWQYO + l+H6Q+kfNglbEzW9AKQjr8kr/eXFDXtWeLuoXzNMAMEjZh2zu1s6Ul3nysB6rCTAcPS+JK/0r+lIdX8i + mUzW7FApAKgRG3YbjM4vZCM9MnBfLgvgWmtmAXhCBv+S4qN7N3hbql8lVJmO1Lz94smu/5FBdruUewY2 + byGe9Fe3J7u/25HoauaZfQB4E/GYt+VAtn5mvi/yrVy27nYZwLXfcMiMAvAn6aW53uinRY+3t/p1gUH4 + vr91W8pv7Uh2fVMG3Z3S9YOCz1WDzZMeiae6Lwo26Wn3/bepXxcAwPCIfm+SdLIM4lPVVsR/3yScq2H1 + C8D6XDq6Un49N5eJzhHLvd3VrwNCxKxZx24b3DsgPT24oa096T9TEo42uq4j4d8lr/C/H090d6ZSKR41 + BYDKIR7wdpKloCnfFzlFBvSSXDZ6l/y6ZpPQrqQrpPqgroQvFm/ay0QWFz/Dz3hN8gp/a/WjgmUEV8BB + MMaT3QuLn30n/QelbwwK0DD5VLALX/Ht/FTX0Z2dcw/gLX0AqDrFdwpWevvLMjC7WAz6I+cVsnXLZID/ + Xjq++wqulerDe/OmI3n59alcpu72YNc9+ddnyqv7Y4tBz0Y8IAlCsz01732dye5ke8r/sgzWJdLri8fY + Jv2XVdjWwuAjjCdkyN8qv/5Ufj9fDe7Qj6f8qdywBwChobA8em8xyIO3838lvVF6i/RW6W+Vd0jvVN6t + vEeall61wdwVdSJ/hfzrwMull0kvlV4s/T/phdILpD+WnhcR4hxvhvoWAMZEW9vHtg+urmU5OCJ4Lj6e + 6j6xI9H9FXnV/YMNjyZ298iAvjlQhXWwW16//Pt/L78+svF/ywC/TX69uSPh/2rDP9N1ZXGDnWTX1+U/ + 9zl5JX9M8N9oT/oHJRJHv4PT9ADACnKZ6J2bXJmPxvOlZ49Nca53uPoWAAAAoNpQAAAAAByEAgAAAOAg + FAAAAAAHoQAAAAA4CAUAAADAQSgAAAAADkIBAAAAcBAKAAAAgINQAAAAAByEAgAAAOAgFAAAAAAHoQAA + AAA4CAUAAADAQSgAAAAADkIBAAAAcBAKAAAAgINQAAAAAByEAgAAAOAgFAAAAAAHoQAAAAA4CAUAAADA + QSgAAAAADkIBAAAAcBAKAAAAgINQAAAAAByEAgAAAOAgFAAAAAAHoQAAAAA4CAUAAADAQSgAAAAADkIB + AAAAcBAKAAAAgINQAAAAAByEAgAAAOAgFAAAAAAHoQAAAAA4CAUAAADAQSgAAAAADkIBAAAAcBAKAAAA + gINQAAAAAByEAgAAAOAgFAAAAAAHoQAAAAA4CAUAAADAQSgAAAAADkIBAAAAcBAKAAAAgINQAAAAAByE + AgAAAOAgFAAAAAAHoQAAAAA4CAUAAADAQSgAAAAADkIBAAAAcBAKAAAAgINQAAAAAByEAgAAAOAgFAAA + AAAHoQAAAAA4CAUAAADAQSgAAAAADkIBAAAAcBAKAAAAgINQAAAAAByEAgAAAOAgFAAAAAAHoQAAAAA4 + CAUAAADAQSgAAAAADjKeAlA4P1LQhftIpAAAAADUkHG+A/CqLtxHIgUAAACghoyrAFwQeVYX7iORAgAA + AFBDZJBfmEtHV47Fwvl1K/JnR/vHojjLO0R9CwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAATi+f9f8YwfCLxZgZEAAAAAElFTkSuQmCC @@ -1425,7 +1747,7 @@ RK5CYII= - + iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAYAAAD0eNT6AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAL FQAACxUBgJnYgwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAGmYSURBVHhe7Z0J @@ -1893,6 +2215,9 @@ AY7pF0Fg9EWkAAAAAElFTkSuQmCC + + 289, 17 + AAABAAEAAAAAAAEAIACvPgAAFgAAAIlQTkcNChoKAAAADUlIRFIAAAEAAAABAAgGAAAAXHKoZgAAPnZJ diff --git a/RPST GUI/RPST/StartForm.vb b/RPST GUI/RPST/StartForm.vb new file mode 100644 index 0000000..5619bba --- /dev/null +++ b/RPST GUI/RPST/StartForm.vb @@ -0,0 +1,105 @@ +Imports System.IO +Imports Newtonsoft.Json +Imports Newtonsoft.Json.Linq + +Public Class StartForm + ReadOnly settings As New SettingsManager() + ReadOnly ApiHandler As New ApiHandler() + + ''' + ''' Handles the click event of the ScrapeButton. + ''' Collects inputs, fetches Reddit posts based on the inputs, + ''' and updates the DataGridView with the fetched posts. + ''' + ''' The sender of the event. + ''' The EventArgs instance containing the event data. + Private Sub ScrapeButton_Click(sender As Object, e As EventArgs) Handles ScrapeButton.Click + Utilities.ProcessRedditPosts(JSONToolStripMenuItem) + End Sub + + + ''' + ''' 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. + ''' + ''' The source of the event. + ''' An EventArgs that contains the event data. + Private Sub StartForm_Load(sender As Object, e As EventArgs) Handles MyBase.Load + settings.LoadSettings() + settings.ToggleDarkMode(settings.DarkMode) + Utilities.PathFinder() + Utilities.LogFirstTimeLaunch() + Me.Text = My.Application.Info.AssemblyName + End Sub + + + ''' + ''' Event handler for the 'About' menu item click. + ''' It shows the 'About' dialog box. + ''' + ''' The source of the event. + ''' An EventArgs that contains the event data. + Private Sub AboutToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles AboutToolStripMenuItem.Click + AboutBox.ShowDialog() + End Sub + + ''' + ''' Event handler for the 'Quit' menu item click. + ''' It asks the user for confirmation and closes the program if the user agrees. + ''' + ''' The source of the event. + ''' An EventArgs that contains the event data. + Private Sub QuitToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles QuitToolStripMenuItem.Click + Dim result As DialogResult = MessageBox.Show("This will close the program, continue?", "Quit", MessageBoxButtons.YesNo, MessageBoxIcon.Question) + If result = DialogResult.Yes Then + Me.Close() + End If + End Sub + + + ''' + ''' Event handler for the 'Developer' menu item click. + ''' It shows the 'Developer' dialog box. + ''' + ''' The source of the event. + ''' An EventArgs that contains the event data. + Private Sub DeveloperToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles DeveloperToolStripMenuItem.Click + DeveloperForm.ShowDialog() + End Sub + + + ''' + ''' Event handler for the 'Check Updates' menu item click. + ''' It checks for application updates and provides update information if a newer version is available. + ''' + ''' The source of the event. + ''' An EventArgs that contains the event data. + Private Sub CheckUpdatesToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles CheckUpdatesToolStripMenuItem.Click + + Dim data As JObject = ApiHandler.CheckUpdates() + If data("tag_name").ToString = $"{My.Application.Info.Version}" Then + MessageBox.Show($"You're running the current version v{My.Application.Info.Version} of {My.Application.Info.ProductName}. Check again soon! :)", "Information", MessageBoxButtons.OK, MessageBoxIcon.Information) + Else + Dim confirm As DialogResult = MessageBox.Show($"A new version v{data("tag_name")} of {My.Application.Info.ProductName} is available, would you like to get it? + +What's new in v{data("tag_name")}? +{data("body")} +", "Update", MessageBoxButtons.YesNo, MessageBoxIcon.Question) + If confirm = DialogResult.Yes Then + Shell($"cmd /c start https://github.com/bellingcat/reddit-post-scraping-tool/releases/tag/{data("tag_name")}") + End If + End If + + End Sub + + + ''' + ''' Event handler for the 'Dark Mode' checkbox change event. + ''' It toggles the dark mode of the application based on the checkbox status. + ''' + ''' The source of the event. + ''' An EventArgs that contains the event data. + Private Sub DarkModeToolStripMenuItem_CheckedChanged(sender As Object, e As EventArgs) Handles DarkModeToolStripMenuItem.CheckedChanged + settings.ToggleDarkMode(DarkModeToolStripMenuItem.Checked) + End Sub +End Class diff --git a/RPST GUI/RPST/Utilities.vb b/RPST GUI/RPST/Utilities.vb new file mode 100644 index 0000000..d2df29f --- /dev/null +++ b/RPST GUI/RPST/Utilities.vb @@ -0,0 +1,180 @@ +Imports System.IO +Imports Newtonsoft.Json +Imports Newtonsoft.Json.Linq + +Public Class Utilities + ''' + ''' 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. + ''' + ''' Indicates whether to save the posts to a JSON file. + ''' + ''' 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. + ''' + Public Shared Sub ProcessRedditPosts(JSONToolStripMenuItem As ToolStripMenuItem) + ' Collect inputs from the user + Dim inputs = CollectInputs() + + If inputs.HasValue Then + ' Initialize the DataGridView + DataGridViewHandler.AddColumn(PostsForm.DataGridViewPosts) + + ' Fetch Reddit posts based on the inputs + Dim processor As New PostsProcessor() + Dim posts As JObject = processor.FetchPosts(inputs.Value.Subreddit, inputs.Value.Listing, inputs.Value.Limit, inputs.Value.Timeframe) + Dim totalPosts As Integer = 0 + Dim keywordFound As Boolean = False + + ' Iterate over each post + For Each post In posts("data")("children") + totalPosts += 1 + ' Check if the post contains the keyword + If PostsProcessor.PostContainsKeyword(post, inputs.Value.Keyword.ToLower(System.Globalization.CultureInfo.InvariantCulture)) Then + ' Add the post to the DataGridView + DataGridViewHandler.AddRow(PostsForm.DataGridViewPosts, post, totalPosts) + PostsForm.Show() + keywordFound = True + End If + Next + + ' Check if the keyword was found in any posts + If Not keywordFound Then + MessageBox.Show($"Keyword `{inputs.Value.Keyword}` was not found in any of the " + posts("data")("children").Count.ToString(System.Globalization.CultureInfo.InvariantCulture) _ + + $" {inputs.Value.Listing} posts from r/{inputs.Value.Subreddit}", "Not Found", MessageBoxButtons.OK, MessageBoxIcon.Warning) + End If + + If JSONToolStripMenuItem.Checked Then + ' Save posts to a JSON file if the JSONToolStripMenuItem is checked + Utilities.SavePostsToJson(posts("data")) + End If + Else + MessageBox.Show("Inputs cannot be empty. Please enter a keyword and a subreddit.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error) + End If + End Sub + + + ''' + ''' 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. + ''' + ''' + ''' The directory path is 'C:\Users\\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. + ''' + 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 + + + ''' + ''' Collects and validates user inputs from StartForm and returns them as a Tuple. + ''' + ''' + ''' Tuple containing: + ''' Keyword (String) - Keyword entered by user in the StartForm. + ''' Subreddit (String) - Subreddit entered by user in the StartForm. + ''' Listing (String) - Listing chosen by user in the StartForm, defaults to 'top' if none is selected. + ''' Limit (Integer) - Limit entered by user in the StartForm, defaults to 10 if the entered value is over 100. + ''' Timeframe (String) - Timeframe chosen by user in the StartForm, defaults to 'all' if none is selected. + ''' + ''' + ''' If keyword or subreddit are empty, Displays a warning and returns nothing. + ''' + Public Shared Function CollectInputs() As (Keyword As String, Subreddit As String, Listing As String, Limit As Integer, Timeframe As String)? + Dim keyword As String = StartForm.KeywordTextBox.Text.Trim() + Dim subreddit As String = StartForm.SubredditTextBox.Text.Trim() + ' Convert the Keyword and Subreddit to lowercase using InvariantCulture + Dim listing As String = If(String.IsNullOrEmpty(StartForm.ListingComboBox.Text), "top", StartForm.ListingComboBox.Text.ToLower(System.Globalization.CultureInfo.InvariantCulture).Trim()) + Dim limit As Integer = StartForm.LimitNumericUpDown.Value + Dim timeframe As String = If(String.IsNullOrEmpty(StartForm.TimeframeComboBox.Text), "all", StartForm.TimeframeComboBox.Text.ToLower(System.Globalization.CultureInfo.InvariantCulture).Trim()) + + ' Validate inputs + If String.IsNullOrEmpty(keyword) Then + MessageBox.Show("Keyword should not be empty", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning) + Return Nothing + End If + If String.IsNullOrEmpty(subreddit) Then + MessageBox.Show("Subreddit should not be empty", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning) + Return Nothing + End If + If limit > 100 Then + MessageBox.Show("Limit should not be over 100. Defaulting to 10", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning) + limit = 10 + End If + + Return (keyword, subreddit, listing, limit, timeframe) + End Function + + + ''' + ''' Saves the gives posts' data to a JSON file. + ''' + ''' The object containing posts to be saved. + ''' + ''' 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. + ''' + Public Shared Sub SavePostsToJson(Posts As Object) + Dim saveFileDialog As New SaveFileDialog With { + .Filter = "JSON files (*.json)|*.json", + .Title = "Save posts to JSON" + } + + If saveFileDialog.ShowDialog() = DialogResult.OK Then + Dim fileName As String = saveFileDialog.FileName + Dim serializerSettings As New JsonSerializerSettings With { + .Formatting = Formatting.Indented + } + Dim json As String = JsonConvert.SerializeObject(Posts, serializerSettings) + + File.WriteAllText(fileName, json) + + MessageBox.Show($"Posts saved to {fileName}", "Success", MessageBoxButtons.OK, MessageBoxIcon.Information) + End If + End Sub + + + ''' + ''' Shows the license notice in a messagebox. + ''' + ''' + ''' The license text is retrieved from the AboutBox.LicenseText property. + ''' The messagebox is displayed with the title "License" and an information icon. + ''' + Public Shared Sub LicenseNotice() + MessageBox.Show(AboutBox.LicenseText, "License", MessageBoxButtons.OK, MessageBoxIcon.Information) + End Sub + + + ''' + ''' Checks if the "first-launch.log" file exists in the directory: C:\Users\\AppData\Roaming\RedditPostScrapingTool\logs. + ''' If the file doesn't exist, it creates one. This file is used to determine whether the program has been run before. + ''' If the program is being run for the first time, a license notice will be displayed. + ''' + Public Shared Sub LogFirstTimeLaunch() + Dim filePath As String = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RPST", "logs", "first-launch.log") + Dim textToWrite As String = $" +{My.Application.Info.AssemblyName} +------------------------- + + +User: {Environment.UserName} +Host: {Environment.MachineName} +OS: {Environment.OSVersion} +x64: {Environment.Is64BitOperatingSystem} +First launched on: {DateTime.Now}" + If Not File.Exists(filePath) Then + LicenseNotice() + File.WriteAllText(filePath, textToWrite) + Else + ' DO NOTHING + End If + End Sub +End Class diff --git a/Reddit Post Scraping Tool/Reddit Post Scraping Tool/icon.ico b/RPST GUI/RPST/icon.ico similarity index 100% rename from Reddit Post Scraping Tool/Reddit Post Scraping Tool/icon.ico rename to RPST GUI/RPST/icon.ico diff --git a/Reddit Post Scraping Tool/Reddit Post Scraping Tool/AboutForm.vb b/Reddit Post Scraping Tool/Reddit Post Scraping Tool/AboutForm.vb deleted file mode 100644 index 9f99189..0000000 --- a/Reddit Post Scraping Tool/Reddit Post Scraping Tool/AboutForm.vb +++ /dev/null @@ -1,14 +0,0 @@ -Public Class AboutForm - Private Sub AboutForm_Load(sender As Object, e As EventArgs) Handles MyBase.Load - DescriptionLabel.Text = "Given a subreddit name and a keyword, -Reddit Post Scraping Tool returns all top posts -(by default) that contain the specified keyword." - VersionLabel.Text = $"v{My.Application.Info.Version}" - LicenseRichTextBox.Text = StartForm.LicenseText - End Sub - - Private Sub LinkLabel1_LinkClicked(sender As Object, e As LinkLabelLinkClickedEventArgs) Handles WikiLinkLabel.LinkClicked - Shell("cmd /c start https://github.com/bellingcat/reddit-post-scraping-tool/wiki") - End Sub - -End Class \ No newline at end of file diff --git a/Reddit Post Scraping Tool/Reddit Post Scraping Tool/ApiHandler.vb b/Reddit Post Scraping Tool/Reddit Post Scraping Tool/ApiHandler.vb deleted file mode 100644 index 51a41ea..0000000 --- a/Reddit Post Scraping Tool/Reddit Post Scraping Tool/ApiHandler.vb +++ /dev/null @@ -1,59 +0,0 @@ -Imports System.IO -Imports System.Net.Http -Imports Newtonsoft.Json -Imports Newtonsoft.Json.Linq - -Public Class ApiHandler - Public logfile As String = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RedditPostScrapingTool", "logs", $"debug.log") - Public headers As String = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1.2 Safari/605.1.15" - Public UpdatesEndpoint As String = "https://api.github.com/repos/bellingcat/reddit-post-scraping-tool/releases/latest" - - Public Function ScrapeReddit(subreddit, listing, limit, timeframe) As JObject - Dim ApiEndpoint As String = $"https://reddit.com/r/{subreddit}/{listing}.json?limit={limit}&t={timeframe}').json()" - Try - Dim httpClient As New HttpClient() - httpClient.DefaultRequestHeaders.Add("User-Agent", headers) - Dim response As HttpResponseMessage = httpClient.GetAsync(ApiEndpoint).Result - If response.IsSuccessStatusCode Then - Dim json As String = response.Content.ReadAsStringAsync().Result - Dim data As JObject = JsonConvert.DeserializeObject(Of JObject)(json) - Return data - Else - ' handle the case when the response status is not successful - ' return an empty JObject or throw an exception - Return New JObject() - MessageBox.Show(response.ReasonPhrase, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error) - End If - Catch ex As Exception - ' handle the exception - ' return an empty JObject or throw an exception - Return New JObject() - My.Computer.FileSystem.WriteAllText(logfile, $"{DateTime.Now}: {ex}{Environment.NewLine}", True) - MessageBox.Show($"{ex.Message}. Please see the debug log '{logfile}' for more information.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error) - End Try - Return New JObject() - End Function - - - ' Gets remote version information from the repository release page - Public Function CheckUpdates() As JObject - Try - Dim httpClient As New HttpClient() - httpClient.DefaultRequestHeaders.Add("User-Agent", headers) - Dim response As HttpResponseMessage = httpClient.GetAsync(UpdatesEndpoint).Result - If response.IsSuccessStatusCode Then - Dim json As String = response.Content.ReadAsStringAsync().Result - Dim data As JObject = JsonConvert.DeserializeObject(Of JObject)(json) - Return data - Else - 'Return New JObject() - MessageBox.Show(response.ReasonPhrase, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error) - End If - Catch ex As Exception - 'Return New JObject() - My.Computer.FileSystem.WriteAllText(logfile, $"{DateTime.Now}: {ex}{Environment.NewLine}", True) - MessageBox.Show($"{ex.Message}. Please see the debug log '{logfile}' for more information.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error) - End Try - Return New JObject() - End Function -End Class diff --git a/Reddit Post Scraping Tool/Reddit Post Scraping Tool/DeveloperForm.Designer.vb b/Reddit Post Scraping Tool/Reddit Post Scraping Tool/DeveloperForm.Designer.vb deleted file mode 100644 index df256b1..0000000 --- a/Reddit Post Scraping Tool/Reddit Post Scraping Tool/DeveloperForm.Designer.vb +++ /dev/null @@ -1,87 +0,0 @@ - -Partial Class DeveloperForm - Inherits System.Windows.Forms.Form - - 'Form overrides dispose to clean up the component list. - - 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. - - Private Sub InitializeComponent() - Dim resources As System.ComponentModel.ComponentResourceManager = New System.ComponentModel.ComponentResourceManager(GetType(DeveloperForm)) - Me.AboutMeLinkLabel = New System.Windows.Forms.LinkLabel() - Me.BuyMeACoffeeLinkLabel = New System.Windows.Forms.LinkLabel() - Me.GreetingLabel = New System.Windows.Forms.Label() - Me.SuspendLayout() - ' - 'AboutMeLinkLabel - ' - Me.AboutMeLinkLabel.AutoSize = True - Me.AboutMeLinkLabel.BackColor = System.Drawing.Color.White - Me.AboutMeLinkLabel.Location = New System.Drawing.Point(33, 426) - Me.AboutMeLinkLabel.Name = "AboutMeLinkLabel" - Me.AboutMeLinkLabel.Size = New System.Drawing.Size(60, 15) - Me.AboutMeLinkLabel.TabIndex = 0 - Me.AboutMeLinkLabel.TabStop = True - Me.AboutMeLinkLabel.Text = "About.me" - ' - 'BuyMeACoffeeLinkLabel - ' - Me.BuyMeACoffeeLinkLabel.AutoSize = True - Me.BuyMeACoffeeLinkLabel.Location = New System.Drawing.Point(33, 451) - Me.BuyMeACoffeeLinkLabel.Name = "BuyMeACoffeeLinkLabel" - Me.BuyMeACoffeeLinkLabel.Size = New System.Drawing.Size(96, 15) - Me.BuyMeACoffeeLinkLabel.TabIndex = 1 - Me.BuyMeACoffeeLinkLabel.TabStop = True - Me.BuyMeACoffeeLinkLabel.Text = "Buy Me A Coffee" - ' - 'GreetingLabel - ' - Me.GreetingLabel.AutoSize = True - Me.GreetingLabel.Font = New System.Drawing.Font("Verdana", 27.75!, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point) - Me.GreetingLabel.Location = New System.Drawing.Point(62, 22) - Me.GreetingLabel.Name = "GreetingLabel" - Me.GreetingLabel.Size = New System.Drawing.Size(382, 45) - Me.GreetingLabel.TabIndex = 3 - Me.GreetingLabel.Text = "Hello, I'm Ritchie" - ' - 'DeveloperForm - ' - Me.AutoScaleDimensions = New System.Drawing.SizeF(7.0!, 15.0!) - Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font - Me.BackgroundImage = CType(resources.GetObject("$this.BackgroundImage"), System.Drawing.Image) - Me.ClientSize = New System.Drawing.Size(510, 510) - Me.Controls.Add(Me.BuyMeACoffeeLinkLabel) - Me.Controls.Add(Me.AboutMeLinkLabel) - Me.Controls.Add(Me.GreetingLabel) - Me.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle - Me.MaximizeBox = False - Me.Name = "DeveloperForm" - Me.ShowIcon = False - Me.ShowInTaskbar = False - Me.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent - Me.Text = "Developer" - Me.ResumeLayout(False) - Me.PerformLayout() - - End Sub - - Friend WithEvents AboutMeLinkLabel As LinkLabel - Friend WithEvents BuyMeACoffeeLinkLabel As LinkLabel - Friend WithEvents PictureBox1 As PictureBox - Friend WithEvents GreetingLabel As Label -End Class diff --git a/Reddit Post Scraping Tool/Reddit Post Scraping Tool/PostsForm.Designer.vb b/Reddit Post Scraping Tool/Reddit Post Scraping Tool/PostsForm.Designer.vb deleted file mode 100644 index dd8e8df..0000000 --- a/Reddit Post Scraping Tool/Reddit Post Scraping Tool/PostsForm.Designer.vb +++ /dev/null @@ -1,56 +0,0 @@ - _ -Partial Class PostsForm - Inherits System.Windows.Forms.Form - - 'Form overrides dispose to clean up the component list. - _ - 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. - _ - Private Sub InitializeComponent() - Me.DataGridViewPosts = New System.Windows.Forms.DataGridView() - CType(Me.DataGridViewPosts, System.ComponentModel.ISupportInitialize).BeginInit() - Me.SuspendLayout() - ' - 'DataGridViewPosts - ' - Me.DataGridViewPosts.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize - Me.DataGridViewPosts.Dock = System.Windows.Forms.DockStyle.Fill - Me.DataGridViewPosts.Location = New System.Drawing.Point(0, 0) - Me.DataGridViewPosts.Name = "DataGridViewPosts" - Me.DataGridViewPosts.ReadOnly = True - Me.DataGridViewPosts.RowTemplate.Height = 25 - Me.DataGridViewPosts.Size = New System.Drawing.Size(800, 450) - Me.DataGridViewPosts.TabIndex = 3 - ' - 'PostsForm - ' - Me.AutoScaleDimensions = New System.Drawing.SizeF(7.0!, 15.0!) - Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font - Me.ClientSize = New System.Drawing.Size(800, 450) - Me.Controls.Add(Me.DataGridViewPosts) - Me.Name = "PostsForm" - Me.ShowIcon = False - Me.ShowInTaskbar = False - Me.Text = "PostsForm" - CType(Me.DataGridViewPosts, System.ComponentModel.ISupportInitialize).EndInit() - Me.ResumeLayout(False) - - End Sub - - Friend WithEvents DataGridViewPosts As DataGridView -End Class diff --git a/Reddit Post Scraping Tool/Reddit Post Scraping Tool/StartForm.vb b/Reddit Post Scraping Tool/Reddit Post Scraping Tool/StartForm.vb deleted file mode 100644 index e2e1ce0..0000000 --- a/Reddit Post Scraping Tool/Reddit Post Scraping Tool/StartForm.vb +++ /dev/null @@ -1,283 +0,0 @@ -Imports System.IO -Imports Newtonsoft.Json -Imports Newtonsoft.Json.Linq - -Public Class StartForm - - ' Create the program's directory - ' We will store information about the user and current machine in it - Public LicenseText As String = "MIT License - -Copyright (c) 2023 Richard Mwewa - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the ""Software""), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE." - Private Sub PathFinder() - Dim directoryPath As String = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RedditPostScrapingTool", "logs") - - If Not Directory.Exists(directoryPath) Then - Directory.CreateDirectory(directoryPath) - Else - ' DO NOTHING - End If - End Sub - - - Private Sub LicenseNotice() - MessageBox.Show(LicenseText, "License", MessageBoxButtons.OK, MessageBoxIcon.Information) - End Sub - - - ' Create a file in C:\Users\\AppData\Roaming\RedditPostScrapingTool, this will be used to determine - ' Whether the program has been run before - ' If it has not been run before, display the license notice - Private Sub LogFirstTimeLaunch() - Dim filePath As String = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RedditPostScrapingTool", "logs", "first_launch.log") - Dim textToWrite As String = $" -{My.Application.Info.AssemblyName} -------------------------- - - -User: {Environment.UserName} -Host: {Environment.MachineName} -OS: {Environment.OSVersion} -x64: {Environment.Is64BitOperatingSystem} -First launched on: {DateTime.Now}" - - If Not File.Exists(filePath) Then - LicenseNotice() - File.WriteAllText(filePath, textToWrite) - Else - ' DO NOTHING - End If - End Sub - - - ' Check the current time - ' add a dark background to the program if it's evening - ' This is my way of implementing auto dark-mode (you could help if you know a better way :) ) - Private Sub DarkModeProperties() - Dim currentHour As Integer = DateTime.Now.Hour - If currentHour >= 6 And currentHour < 18 Then - Me.BackColor = ColorTranslator.FromHtml("#FFFFFFFF") - - ToolsToolStripMenuTools.ForeColor = ColorTranslator.FromHtml("#FF121212") - KeywordTextBox.BackColor = ColorTranslator.FromHtml("#FFFFFFFF") - KeywordTextBox.ForeColor = ColorTranslator.FromHtml("#FF121212") - - SubredditTextBox.BackColor = ColorTranslator.FromHtml("#FFFFFFFF") - SubredditTextBox.ForeColor = ColorTranslator.FromHtml("#FF121212") - - LimitNumericUpDown.BackColor = ColorTranslator.FromHtml("#FFFFFFFF") - LimitNumericUpDown.ForeColor = ColorTranslator.FromHtml("#FF121212") - - LimitNumericUpDown.BackColor = ColorTranslator.FromHtml("#FFFFFFFF") - LimitNumericUpDown.ForeColor = ColorTranslator.FromHtml("#FF121212") - - ListingComboBox.BackColor = ColorTranslator.FromHtml("#FFFFFFFF") - ListingComboBox.ForeColor = ColorTranslator.FromHtml("#FF121212") - - TimeframeComboBox.BackColor = ColorTranslator.FromHtml("#FFFFFFFF") - TimeframeComboBox.ForeColor = ColorTranslator.FromHtml("#FF121212") - - Label1.ForeColor = ColorTranslator.FromHtml("#FF121212") - Label2.ForeColor = ColorTranslator.FromHtml("#FF121212") - Label3.ForeColor = ColorTranslator.FromHtml("#FF121212") - Label4.ForeColor = ColorTranslator.FromHtml("#FF121212") - Label5.ForeColor = ColorTranslator.FromHtml("#FF121212") - Else - Me.BackColor = ColorTranslator.FromHtml("#FF121212") - - ToolsToolStripMenuTools.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF") - KeywordTextBox.BackColor = ColorTranslator.FromHtml("#FF2E2E2E") - KeywordTextBox.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF") - - SubredditTextBox.BackColor = ColorTranslator.FromHtml("#FF2E2E2E") - SubredditTextBox.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF") - - LimitNumericUpDown.BackColor = ColorTranslator.FromHtml("#FF2E2E2E") - LimitNumericUpDown.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF") - LimitNumericUpDown.BackColor = ColorTranslator.FromHtml("#FF2E2E2E") - LimitNumericUpDown.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF") - - ListingComboBox.BackColor = ColorTranslator.FromHtml("#FF2E2E2E") - ListingComboBox.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF") - - TimeframeComboBox.BackColor = ColorTranslator.FromHtml("#FF2E2E2E") - TimeframeComboBox.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF") - - Label1.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF") - Label2.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF") - Label3.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF") - Label4.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF") - Label5.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF") - End If - End Sub - - - Private Sub ScrapeButton_Click(sender As Object, e As EventArgs) Handles ScrapeButton.Click - Dim ApiHandler As New ApiHandler - Dim Keyword As String = KeywordTextBox.Text - Dim Subreddit As String = SubredditTextBox.Text - Dim Listing As String = ListingComboBox.Text.ToLower() - Dim Limit As Integer = LimitNumericUpDown.Value - Dim Timeframe As String = TimeframeComboBox.Text.ToLower() - Dim FoundPosts As Integer = 0 - Dim TotalPosts As Integer = 0 - - ' Clear the Columns and Rows before adding Items to them - PostsForm.DataGridViewPosts.Rows.Clear() - PostsForm.DataGridViewPosts.Columns.Clear() - - PostsForm.DataGridViewPosts.Columns.Add("PostCount", "Post Number") - PostsForm.DataGridViewPosts.Columns.Add("PostAuthor", "Author") - PostsForm.DataGridViewPosts.Columns.Add("PostID", "ID") - PostsForm.DataGridViewPosts.Columns.Add("PostSubreddit", "Subreddit") - PostsForm.DataGridViewPosts.Columns.Add("SubredditVisibility", "Subreddit Visibility") - PostsForm.DataGridViewPosts.Columns.Add("PostThumbnail", "Thumbnail") - PostsForm.DataGridViewPosts.Columns.Add("PostIsNSFW", "NSFW") - PostsForm.DataGridViewPosts.Columns.Add("PostIsGilded", "Gilded") - PostsForm.DataGridViewPosts.Columns.Add("PostUpvotes", "Upvotes") - PostsForm.DataGridViewPosts.Columns.Add("PostUpvoteRatio", "Upvote Ratio") - PostsForm.DataGridViewPosts.Columns.Add("PostDownvotes", "Downvotes") - PostsForm.DataGridViewPosts.Columns.Add("PostAwards", "Awards") - PostsForm.DataGridViewPosts.Columns.Add("PostTopAward", "Top Award") - PostsForm.DataGridViewPosts.Columns.Add("PostIsCrosspostable", "Is Crosspostable?") - PostsForm.DataGridViewPosts.Columns.Add("PostScore", "Score") - PostsForm.DataGridViewPosts.Columns.Add("PostText", "Text") - PostsForm.DataGridViewPosts.Columns.Add("PostCategory", "Category") - PostsForm.DataGridViewPosts.Columns.Add("PostDomain", "Domain") - PostsForm.DataGridViewPosts.Columns.Add("PostPermalink", "Permalink") - PostsForm.DataGridViewPosts.Columns.Add("PostCreatedAt", "Created At") - PostsForm.DataGridViewPosts.Columns.Add("PostApprovedAt", "Approved At") - PostsForm.DataGridViewPosts.Columns.Add("PostApprovedBy", "Approved By") - - If Limit > 100 Then - MessageBox.Show("Limit should not be over 100. Defaulting to 10", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning) - End If - - If Listing = "" Then - Listing = "top" - End If - - If Timeframe = "" Then - Timeframe = "all" - End If - - If Keyword = "" Then - MessageBox.Show("Keyword should not be emtpy", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning) - ElseIf Subreddit = "" Then - MessageBox.Show("Subreddit should not be emtpy", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning) - Else - PostsForm.Text = $"Reddit Post Scraping Tool - {Keyword}" - Dim Posts As JObject = ApiHandler.ScrapeReddit(Subreddit, Listing, Limit, Timeframe) - For Each Post In Posts("data")("children") - TotalPosts += 1 - If Post("data")("selftext").ToString.ToLower().Contains(KeywordTextBox.Text.ToLower()) Then - FoundPosts += 1 - PostsForm.DataGridViewPosts.Rows.Add(TotalPosts, Post("data")("author"), Post("data")("id"), Post("data")("subreddit_name_prefixed"), - Post("data")("subreddit_type"), Post("data")("thumbnail"), Post("data")("over_18"), Post("data")("gilded"), - Post("data")("ups"), Post("data")("upvote_ratio"), Post("data")("downs"), Post("data")("total_awards_received"), - Post("data")("top_awarded_type"), Post("data")("is_crosspostable"), Post("data")("score"), Post("data")("selftext"), - Post("data")("category"), Post("data")("domain"), Post("data")("permalink"), Post("data")("created"), - Post("data")("approved_at_utc"), Post("data")("approved_by")) - End If - Next - - 'Don't show the results form if found posts are not greater than 0 - If FoundPosts > 0 Then - MessageBox.Show($"Keyword `{Keyword}` was found in {FoundPosts}/" + Posts("data")("children").Count.ToString _ - + $" {Listing} posts from r/{Subreddit}", "Found", MessageBoxButtons.OK, MessageBoxIcon.Information) - PostsForm.Show() - Else - MessageBox.Show($"Keyword `{Keyword}` was not found in either one of the " + Posts("data")("children").Count.ToString _ - + $" {Listing} posts from r/{Subreddit}", "Not Found", MessageBoxButtons.OK, MessageBoxIcon.Warning) - End If - - - If JSONToolStripMenuItem.Checked Then - Dim saveFileDialog As New SaveFileDialog() - - saveFileDialog.Filter = "JSON files (*.json)|*.json" - saveFileDialog.Title = "Save posts to JSON" - - If saveFileDialog.ShowDialog() = DialogResult.OK Then - Dim fileName As String = saveFileDialog.FileName - Dim serializerSettings As New JsonSerializerSettings() - serializerSettings.Formatting = Formatting.Indented - Dim json As String = JsonConvert.SerializeObject(Posts("data"), serializerSettings) - - System.IO.File.WriteAllText(fileName, json) - - MessageBox.Show($"Results saved to {fileName} successfully!", "Success", MessageBoxButtons.OK, MessageBoxIcon.Information) - End If - End If - End If - End Sub - - ' StartForm load event - Private Sub StartForm_Load(sender As Object, e As EventArgs) Handles MyBase.Load - PathFinder() - LogFirstTimeLaunch() - DarkModeProperties() - ToolsToolStripMenuTools.Text = Environment.UserName - Me.Text = $"{My.Application.Info.AssemblyName} v{My.Application.Info.Version}" - End Sub - - Private Sub AboutToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles AboutToolStripMenuItem.Click - AboutForm.ShowDialog() - End Sub - - Private Sub QuitToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles QuitToolStripMenuItem.Click - Dim result As DialogResult = MessageBox.Show("This will close the program, continue?", "Quit", MessageBoxButtons.YesNo, MessageBoxIcon.Question) - If result = DialogResult.Yes Then - Me.Close() - End If - End Sub - - Private Sub DeveloperToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles DeveloperToolStripMenuItem.Click - DeveloperForm.ShowDialog() - End Sub - - Private Sub ChekUpdatesToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles ChekUpdatesToolStripMenuItem.Click - Dim ApiHandler As New ApiHandler() - Dim data As JObject = ApiHandler.CheckUpdates() - If data("tag_name").ToString = $"{My.Application.Info.Version}" Then - MessageBox.Show($"You're running the current version v{My.Application.Info.Version} of {My.Application.Info.ProductName}. Check again soon! :)", "Information", MessageBoxButtons.OK, MessageBoxIcon.Information) - Else - Dim confirm As DialogResult = MessageBox.Show($"A new version v{data("tag_name")} of {My.Application.Info.ProductName} is availble, would you like to get it? - -What's new in v{data("tag_name")}? -{data("body")} -", "Update", MessageBoxButtons.YesNo, MessageBoxIcon.Question) - If confirm = DialogResult.Yes Then - Shell($"cmd /c start https://github.com/bellingcat/reddit-post-scraping-tool/releases/tag/{data("tag_name")}") - End If - End If - - End Sub - - Private Sub ToolsToolStripMenuTools_Click(sender As Object, e As EventArgs) Handles ToolsToolStripMenuTools.Click - ToolsToolStripMenuTools.ForeColor = ColorTranslator.FromHtml("#FF121212") - End Sub - - Private Sub ToolsToolStripMenuTools_DropDownClosed(sender As Object, e As EventArgs) Handles ToolsToolStripMenuTools.DropDownClosed - ToolsToolStripMenuTools.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF") - End Sub - -End Class diff --git a/pyproject.toml b/pyproject.toml index 2efddc9..3b4034a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,8 +7,8 @@ packages = ["reddit_post_scraping_tool"] [project] name = "reddit-post-scraping-tool" -version = "1.3.0.1" -description = "Given a subreddit name and a keyword, this program returns all top (by default) posts that contain the specified word." +version = "1.4.0.0" +description = "Given a subreddit name and a keyword, RPST returns all top (by default) posts that contain the specified keyword." readme = "README.md" requires-python = ">=3.8" license = {file = "LICENSE"} @@ -30,9 +30,9 @@ dependencies = [ ] [project.urls] -homepage = "https://github.com/bellingcat/reddit-post-scraping-tool" +homepage = "https://github.com/bellingcat" documentation = "https://github.com/bellingcat/reddit-post-scraping-tool/wiki" repository = "https://github.com/bellingcat/reddit-post-scraping-tool.git" [project.scripts] -reddit_post_scraping_tool = "reddit_post_scraping_tool.main:main" +rpst = "rpst.__main:run" diff --git a/reddit_post_scraping_tool/main.py b/reddit_post_scraping_tool/main.py deleted file mode 100644 index d10d189..0000000 --- a/reddit_post_scraping_tool/main.py +++ /dev/null @@ -1,13 +0,0 @@ -from reddit_post_scraping_tool.reddit_post_scraping_tool import * - - -def main(): - try: - check_updates("1.3.0.1") - reddit_post_scraper() - except KeyboardInterrupt: - log.warning(f"User interruption detected.") - except Exception as e: - log.error(e) - finally: - log.info(f'Finished in {datetime.now() - start_time} seconds.') diff --git a/reddit_post_scraping_tool/reddit_post_scraping_tool.py b/reddit_post_scraping_tool/reddit_post_scraping_tool.py deleted file mode 100644 index 9b656e9..0000000 --- a/reddit_post_scraping_tool/reddit_post_scraping_tool.py +++ /dev/null @@ -1,91 +0,0 @@ -import logging -import argparse -import requests -from rich.tree import Tree -from datetime import datetime -from rich import print as xprint -from rich.markdown import Markdown -from rich.logging import RichHandler - - -start_time = datetime.now() -logging.basicConfig(level="NOTSET", format="%(message)s", handlers=[RichHandler(markup=True, log_time_format='[%H:%M:%S%p]')]) -log = logging.getLogger("rich") - - -# Check if the remote tag_name from the latest release matches the one in the program -# if it does, it means the program is up-to-date. -# If it doesn't match, notify the user about a new release -def check_updates(version_tag): - response = requests.get("https://api.github.com/repos/bellingcat/reddit-post-scraping-tool/releases/latest").json() - if response['tag_name'] == version_tag: - pass - else: - raw_release_notes = response['body'] - markdown_release_notes = Markdown(raw_release_notes) - log.info(f"A new release of reddit-post-scraping-tool is available ({response['tag_name']}). Run 'pip install --upgrade reddit-post-scraping-tool' to get the updates.") - xprint(markdown_release_notes) - - -# Getting posts -def get_posts(post): - post_data = {'Author': post['data']['author'], - 'ID': post['data']['id'], - 'Subreddit': post["data"]["subreddit_name_prefixed"], - 'Visibility': post['data']['subreddit_type'], - # 'Author': post["data"]["author_fullname"], - 'Thumbnail': post["data"]["thumbnail"], - # 'Flair': post["data"]["link_flair_text"], - 'NSFW': post['data']['over_18'], - 'Gilded': post['data']['gilded'], - 'Upvotes': post["data"]["ups"], - 'Upvote ratio': post["data"]["upvote_ratio"], - 'Downvotes': post["data"]["downs"], - 'Awards': post["data"]["total_awards_received"], - 'Top award': post['data']['top_awarded_type'], - 'Is crosspostable?': post['data']['is_crosspostable'], - 'Score': post["data"]["score"], - 'Category': post['data']['category'], - 'Domain': post["data"]["domain"], - 'Created': post['data']['created'], - 'Approved at': post['data']['approved_at_utc'], - 'Approved by': post['data']['approved_by'], } - - post_tree = Tree("\n" + post['data']['title']) - for post_key, post_value in post_data.items(): - post_tree.add(f"{post_key}: {post_value}") - xprint(post_tree) - print(post['data']['selftext'] + "\n") - - -def reddit_post_scraper(): - session = requests.session() - session.headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15'} - response = session.get(f'https://reddit.com/r/{args.subreddit}/{args.listing}.json?limit={args.limit}&t={args.timeframe}').json() - found_posts = 0 - for post in response['data']['children']: - if args.keyword.lower() in post['data']['selftext'] or args.keyword.lower() in post['data']['title']: - found_posts += 1 - get_posts(post) - - log.info(f"Keyword ('{args.keyword}') was found in {found_posts}/{len(response['data']['children'])} {args.listing} posts from r/{args.subreddit}.") - - -def create_parser(): - parser = argparse.ArgumentParser( - description=f'reddit-post-scraping-tool — by Richard Mwewa | https://about.me/rly0nheart', - epilog=f'Given a subreddit name and a keyword, this program returns all top (by default) posts that contain the specified word. ') - parser.add_argument('-k', '--keyword', help='kewyword', required=True) - parser.add_argument('-s', '--subreddit', help='subreddit', required=True) - parser.add_argument('-c', '--limit', help='results limit (1-100) (default: %(default)s)', default=10, type=int) - parser.add_argument('-l', '--listing', default='top', const='top', nargs='?', - choices=['controversial', 'hot', 'best', 'new', 'rising'], - help='listings: controversial, hot, best, new, rising (default: %(default)s)') - parser.add_argument('-t', '--timeframe', default='all', const='all', nargs='?', - choices=['hour', 'day', 'week', 'month', 'year'], - help='timeframe: hour, day, week, month, year (default: %(default)s)') - return parser - - -_parser = create_parser() -args = _parser.parse_args() diff --git a/rpst/__main.py b/rpst/__main.py new file mode 100644 index 0000000..4b28fb6 --- /dev/null +++ b/rpst/__main.py @@ -0,0 +1,30 @@ +from datetime import datetime +from rpst.__rpst import log, start_scraper, check_updates, create_parser + + +def run(): + """ + Main entry point for the program. It creates a parser, parses the command line arguments, + checks for updates, starts the scraper, 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() + + # Record the start time + start_time = datetime.now() + + try: + # Check for updates + check_updates(version_tag="1.4.0.0") + + # Start the scraper with the parsed arguments + start_scraper(keyword=args.keyword, subreddit=args.subreddit, + listing=args.listing, timeframe=args.timeframe, limit=args.limit) + except KeyboardInterrupt: + log.warning("User interruption detected.") + except Exception as e: + log.error(f"An error occurred: {e}") + finally: + log.info(f'Finished in {datetime.now() - start_time} seconds.') diff --git a/rpst/__rpst_.py b/rpst/__rpst_.py new file mode 100644 index 0000000..5e2bef1 --- /dev/null +++ b/rpst/__rpst_.py @@ -0,0 +1,165 @@ +import logging +import argparse +import requests +from rich.tree import Tree +from rich import print as xprint +from rich.markdown import Markdown +from rich.logging import RichHandler + + +def check_updates(version_tag: str): + """ + Checks if there's a new release of a project on GitHub. If there is, it logs an + information message and prints the release notes. + + :param version_tag: A string representing the current version of the project. + """ + + # Make a GET request to the GitHub API to get the latest release of the project + response = requests.get("https://api.github.com/repos/bellingcat/reddit-post-scraping-tool/releases/latest").json() + + # Check if the latest release's tag matches the current version tag + if response['tag_name'] != version_tag: + + # If not, convert the release notes from Markdown to HTML + raw_release_notes = response['body'] + markdown_release_notes = Markdown(raw_release_notes) + + # Log an info message about the new release + log.info( + f"A new release of RPST is available ({response['tag_name']}). " + f"Run 'pip install --upgrade reddit-post-scraping-tool' to get the updates." + ) + + # Print the release notes + xprint(markdown_release_notes) + + +def get_post_data(post: dict): + """ + Extracts relevant data from a Reddit post and displays it in a tree structure, + followed by the post's selftext. + + :param post: A dictionary containing the data of a Reddit post. + """ + # Define the data to extract from the post + post_data = { + 'Author': post['data']['author'], + 'ID': post['data']['id'], + 'Subreddit': post["data"]["subreddit_name_prefixed"], + 'Visibility': post['data']['subreddit_type'], + 'Thumbnail': post["data"]["thumbnail"], + 'NSFW': post['data']['over_18'], + 'Gilded': post['data']['gilded'], + 'Upvotes': post["data"]["ups"], + 'Upvote ratio': post["data"]["upvote_ratio"], + 'Downvotes': post["data"]["downs"], + 'Awards': post["data"]["total_awards_received"], + 'Top award': post['data']['top_awarded_type'], + 'Is crosspostable?': post['data']['is_crosspostable'], + 'Score': post["data"]["score"], + 'Category': post['data']['category'], + 'Domain': post["data"]["domain"], + 'Created': post['data']['created'], + 'Approved at': post['data']['approved_at_utc'], + 'Approved by': post['data']['approved_by'], + } + + # Create a tree structure with the post's title as the root + post_tree = Tree("\n" + post['data']['title']) + + # Add each piece of extracted data as a branch of the tree + for post_key, post_value in post_data.items(): + post_tree.add(f"{post_key}: {post_value}") + + # Print the tree structure + xprint(post_tree) + + # Print the post's selftext + print(post['data']['selftext'] + "\n") + + +def start_scraper(keyword: str, subreddit: str, listing: str, timeframe: str, limit: int): + """ + Scrapes a given subreddit for posts that contain a specified keyword. + The search is limited by the number of posts and timeframe specified. + + :param keyword: The keyword to search for in the posts. + :param subreddit: The subreddit to scrape. + :param listing: The type of posts to scrape. This could be 'hot', 'new', etc. + :param timeframe: The timeframe from which to scrape posts. This could be 'day', 'week', etc. + :param limit: The maximum number of posts to scrape. + + This function logs the number of posts in which the keyword was found. + """ + + # Start a new session + session = requests.session() + # Set the User-Agent to mimic a Safari browser on a Mac + session.headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, ' + 'like Gecko) Version/14.1.1 Safari/605.1.15'} + + # Send a GET request to the specified subreddit and listing, + # limiting the response by the specified limit and timeframe + response = session.get(f'https://reddit.com/r/{subreddit}/{listing}.json?limit={limit}&t={timeframe}').json() + + # Initialize a counter for the number of posts found that contain the keyword + found_posts = 0 + + # Loop through each post in the response + for post in response['data']['children']: + # If the keyword is found in the post's selftext or title, increment the counter and process the post + if keyword.lower() in post['data']['selftext'] or keyword.lower() in post['data']['title']: + found_posts += 1 + get_post_data(post=post) + + # Log the number of posts in which the keyword was found + log.info(f"Keyword ('{keyword}') was found in {found_posts}/{len(response['data']['children'])} " + f"{listing} posts from r/{subreddit}.") + + +def create_parser(): + """ + Creates and configures an argument parser for the command line arguments. + + :return: A configured argparse.ArgumentParser object ready to parse the command line arguments. + """ + parser = argparse.ArgumentParser( + description='RPST: Reddit Post Scraping Tool —by Richard Mwewa | https://about.me/rly0nheart', + epilog='Given a subreddit name and a keyword, ' + 'RPST returns all top (by default) posts that contain the specified keyword.' + ) + + parser.add_argument('-k', '--keyword', help='The keyword to search for in the posts.', required=True) + parser.add_argument('-s', '--subreddit', help='The subreddit to scrape.', required=True) + parser.add_argument( + '-c', '--limit', + help='The maximum number of posts to scrape (1-100). (default: %(default)s)', + default=10, + const=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)' + ) + + return parser + + +logging.basicConfig(level="NOTSET", format="%(message)s", + handlers=[RichHandler(markup=True, log_time_format='[%H:%M:%S%p]')]) +log = logging.getLogger("rich")