47 Commits

Author SHA1 Message Date
Richard Mwewa
750967c322 Merge pull request #12 from bellingcat/dev
Dev
2023-08-25 15:53:49 +02:00
Richard Mwewa
cfef86cbe3 Update README.md 2023-08-25 15:53:23 +02:00
Richard Mwewa
2a2696403d Update README.md 2023-08-25 15:52:49 +02:00
Richard Mwewa
b0a8d75d8c Update README.md 2023-08-25 15:49:07 +02:00
Richard Mwewa
b31c38f5cc Update README.md 2023-08-25 15:48:19 +02:00
Richard Mwewa
b5b7df868e Update README.md 2023-08-25 15:44:13 +02:00
Richard Mwewa
566f558720 Update utils.py 2023-08-25 15:27:18 +02:00
Richard Mwewa
c3e5ce6441 Update rpst.py 2023-08-25 15:26:42 +02:00
Richard Mwewa
7c164938c9 Update RPST.vbproj 2023-08-25 15:06:31 +02:00
Richard Mwewa
b08c4a147b Create utils.py 2023-08-25 15:04:37 +02:00
Richard Mwewa
8f259b7a40 Update pyproject.toml
1.7.0.0
2023-08-25 14:54:31 +02:00
Richard Mwewa
f117c99cc7 Update and rename __main.py to main.py
1.7.0.0
2023-08-25 14:52:27 +02:00
Richard Mwewa
3a9a87e67c Update and rename __rpst.py to rpst.py
1.7.0.0
2023-08-25 14:51:16 +02:00
Richard Mwewa
cce254e976 Update pyproject.toml 2023-08-14 02:51:49 +02:00
Richard Mwewa
418b2acc4c Update README.md 2023-08-12 05:25:07 +02:00
Richard Mwewa
d26699cc1f Update README.md 2023-08-12 05:24:35 +02:00
Richard Mwewa
9efb1cea4a Merge pull request #10 from bellingcat/dev
Dev
2023-08-12 05:19:16 +02:00
Richard Mwewa
ba6eeb38a6 Add files via upload 2023-08-12 05:09:57 +02:00
Richard Mwewa
2053c0f0bc Update pyproject.toml 2023-08-12 05:05:23 +02:00
Richard Mwewa
8bef73001c Update __main.py 2023-08-12 05:04:56 +02:00
Richard Mwewa
c9d9628326 Update __rpst.py
Saved posts will also include the selftext.
2023-08-12 05:04:24 +02:00
Richard Mwewa
2f1619b4c5 Merge pull request #9 from bellingcat/dev
Dev
2023-08-12 03:54:42 +02:00
Richard Mwewa
33db66dbc3 Add files via upload 2023-08-12 03:53:33 +02:00
Richard Mwewa
bbbdab906d Update pyproject.toml 2023-08-12 03:49:05 +02:00
Richard Mwewa
74264224a5 Update __main.py 2023-08-12 03:47:28 +02:00
Richard Mwewa
ce75d40f76 Update __rpst.py
Changed post ouput format
2023-08-12 03:46:23 +02:00
Richard Mwewa
406e34c4bb Update __rpst.py 2023-08-09 22:50:33 +02:00
Richard Mwewa
38140ea2be Merge pull request #8 from bellingcat/dev
Update and rename __rpst_.py to __rpst.py
2023-08-09 22:39:33 +02:00
Richard Mwewa
4c3d3a688f Update and rename __rpst_.py to __rpst.py
Yep, I suck
2023-08-09 22:38:48 +02:00
Richard Mwewa
a03b649904 Merge pull request #7 from bellingcat/dev
Create __init__.py
2023-08-09 22:36:41 +02:00
Richard Mwewa
aa3b506a96 Create __init__.py 2023-08-09 22:36:20 +02:00
Richard Mwewa
13db97b6d8 Merge pull request #6 from bellingcat/dev
Dev
2023-08-09 04:45:56 +02:00
Richard Mwewa
4da2fcf913 Update __main.py 2023-08-09 04:45:13 +02:00
Richard Mwewa
c6792277f3 Update pyproject.toml 2023-08-09 04:44:42 +02:00
Richard Mwewa
5bc061b300 Update README.md 2023-08-09 04:44:07 +02:00
Richard Mwewa
b3441a58b1 Update README.md 2023-08-09 04:43:43 +02:00
Richard Mwewa
60ba2e41b0 Update README.md 2023-08-09 04:17:17 +02:00
Richard Mwewa
7bebff61a8 Update README.md 2023-08-09 04:16:39 +02:00
Richard Mwewa
3576bcbf45 1.6.0.0
Added Tool tips on the Main Form controls and auto complete on the Listing and Timeframe controls.
2023-08-09 04:14:00 +02:00
Richard Mwewa
d266301917 Delete RPST GUI directory 2023-08-09 04:11:12 +02:00
Richard Mwewa
1f3d8f41eb Merge pull request #5 from bellingcat/dev
Dev
2023-08-08 07:15:29 +02:00
Richard Mwewa
4ac58f0fc4 Update README.md 2023-08-08 07:10:13 +02:00
Richard Mwewa
7696dd923a Update __main.py
Changed layout and applied dark mode to the "Right Click" menu
2023-08-08 07:08:58 +02:00
Richard Mwewa
45b82e57ac Update pyproject.toml
Changed layout and applied dark mode to the "Right Click" Menu
2023-08-08 07:07:52 +02:00
Richard Mwewa
6d6e616640 Update README.md 2023-08-08 07:06:52 +02:00
Richard Mwewa
4ba402a129 Add files via upload
1.5.0.0
2023-08-08 07:00:32 +02:00
Richard Mwewa
52d36baae2 Delete RPST GUI directory 2023-08-07 16:29:00 +02:00
30 changed files with 2003 additions and 1664 deletions

View File

@@ -1,23 +1,18 @@
# RPST (Reddit Post Scraping Tool)
Given a subreddit name and a keyword, this script will return all posts from a specified listing (default is 'top') that contain the provided keyword.
[![Upload Python Package](https://github.com/rly0nheart/reddit-post-scraping-tool/actions/workflows/python-publish.yml/badge.svg)](https://github.com/rly0nheart/reddit-post-scraping-tool/actions/workflows/python-publish.yml) [![CodeQL](https://github.com/rly0nheart/reddit-post-scraping-tool/actions/workflows/codeql.yml/badge.svg)](https://github.com/rly0nheart/reddit-post-scraping-tool/actions/workflows/codeql.yml) ![.Net](https://img.shields.io/badge/.NET-5C2D91?style=flat&logo=.net&logoColor=white) ![Python](https://img.shields.io/badge/python-3670A0?style=flat&logo=python&logoColor=ffdd54)
![2023-08-07_02-13_1](https://github.com/bellingcat/reddit-post-scraping-tool/assets/74001397/5ea98745-8b5f-4a93-9a53-befa491f7b6a)
![2023-08-07_02-13](https://github.com/bellingcat/reddit-post-scraping-tool/assets/74001397/f303abc7-8a83-44b0-97c9-a447c459cef9)
Given a subreddit name and a keyword, RPST will return all posts from a specified listing (default is 'top') that contain the provided keyword.
[![Upload Python Package](https://github.com/bellingcat/reddit-post-scraping-tool/actions/workflows/python-publish.yml/badge.svg)](https://github.com/rly0nheart/reddit-post-scraping-tool/actions/workflows/python-publish.yml) [![CodeQL](https://github.com/bellingcat/reddit-post-scraping-tool/actions/workflows/codeql.yml/badge.svg)](https://github.com/rly0nheart/reddit-post-scraping-tool/actions/workflows/codeql.yml) ![.Net](https://img.shields.io/badge/.NET-5C2D91?style=flat&logo=.net&logoColor=white) ![Python](https://img.shields.io/badge/python-3670A0?style=flat&logo=python&logoColor=ffdd54)
# ✅ Features
## GUI
- [x] Dark mode (Right-click)
- [x] Saves results to a JSON (Right-click)
- [x] Dark mode (*Right-click*)
- [x] Saves results to a JSON file (*Right-click*)
- [x] Logs errors to a file
## CLI
- [x] Saves results to a JSON (-j/--json)
- [x] Automatically checks for new updates. Notifies user if update were found.
- [x] Saves results to JSON (*specifiy* `--json`)
- [x] Saves results to CSV (*specify* `--csv`)
- [x] Automatically checks for new updates, and notifies user if updates were found.
# 📃 TODO
## GUI
@@ -25,8 +20,21 @@ Given a subreddit name and a keyword, this script will return all posts from a s
- [x] Add manual dark mode option, that will be persistent in all sessions
- [ ] Make it save results to a CSV file
# Images & Screenshots
## GUI
* ![2023-08-09_04-05](https://github.com/bellingcat/reddit-post-scraping-tool/assets/74001397/d8917a35-3eac-44ce-aa96-1f9685095254)
* ![2023-08-09_04-05_1](https://github.com/bellingcat/reddit-post-scraping-tool/assets/74001397/d2fe7269-91d4-49ad-87fb-44282c5637a7)
## CLI
* ![2023-08-25_15-39](https://github.com/bellingcat/reddit-post-scraping-tool/assets/74001397/4bca09b3-271f-452d-81a7-39c9986539f2)
* ![2023-08-25_15-30](https://github.com/bellingcat/reddit-post-scraping-tool/assets/74001397/2b39bdfa-87d0-4038-90cd-14e7d3b6a84b)
* ![2023-08-25_15-35](https://github.com/bellingcat/reddit-post-scraping-tool/assets/74001397/47ba23ad-8d32-49c5-8c16-34a903fbc581)
# 📖 Wiki
[Refer to the Wiki](https://github.com/rly0nheart/reddit-post-scraping-tool/wiki) for installation instructions, in addition to all other documentation.
[Refer to the Wiki](https://github.com/bellingcat/reddit-post-scraping-tool/wiki) for installation instructions, in addition to all other documentation.
# 😁 Donations
If you like `RPST` and would like to show support, you can Buy A Coffee for the developer using the button below

View File

@@ -40,7 +40,7 @@ Partial Class AboutBox
PictureBoxLogo.Image = CType(resources.GetObject("PictureBoxLogo.Image"), Image)
PictureBoxLogo.Location = New Point(12, 12)
PictureBoxLogo.Name = "PictureBoxLogo"
PictureBoxLogo.Size = New Size(99, 111)
PictureBoxLogo.Size = New Size(88, 93)
PictureBoxLogo.SizeMode = PictureBoxSizeMode.StretchImage
PictureBoxLogo.TabIndex = 0
PictureBoxLogo.TabStop = False
@@ -48,43 +48,43 @@ Partial Class AboutBox
' LabelProgramName
'
LabelProgramName.AutoSize = True
LabelProgramName.Font = New Font("Ink Free", 14.25F, FontStyle.Bold, GraphicsUnit.Point)
LabelProgramName.Font = New Font("Segoe Script", 9.75F, FontStyle.Bold, GraphicsUnit.Point)
LabelProgramName.ForeColor = SystemColors.ControlText
LabelProgramName.Location = New Point(4, 11)
LabelProgramName.Location = New Point(3, 15)
LabelProgramName.Name = "LabelProgramName"
LabelProgramName.Size = New Size(60, 23)
LabelProgramName.Size = New Size(48, 20)
LabelProgramName.TabIndex = 3
LabelProgramName.Text = "Name"
'
' LabelProgramDescription
'
LabelProgramDescription.AutoSize = True
LabelProgramDescription.Font = New Font("Comic Sans MS", 9F, FontStyle.Regular, GraphicsUnit.Point)
LabelProgramDescription.Font = New Font("Segoe UI Semibold", 9F, FontStyle.Bold, GraphicsUnit.Point)
LabelProgramDescription.ForeColor = SystemColors.ControlText
LabelProgramDescription.Location = New Point(4, 54)
LabelProgramDescription.Location = New Point(3, 43)
LabelProgramDescription.Name = "LabelProgramDescription"
LabelProgramDescription.Size = New Size(73, 17)
LabelProgramDescription.Size = New Size(68, 15)
LabelProgramDescription.TabIndex = 4
LabelProgramDescription.Text = "Description"
'
' LabelVersion
'
LabelVersion.AutoSize = True
LabelVersion.Font = New Font("Comic Sans MS", 9F, FontStyle.Underline, GraphicsUnit.Point)
LabelVersion.Font = New Font("Segoe UI", 9F, FontStyle.Underline, GraphicsUnit.Point)
LabelVersion.ForeColor = SystemColors.ControlText
LabelVersion.Location = New Point(372, 17)
LabelVersion.Location = New Point(347, 17)
LabelVersion.Name = "LabelVersion"
LabelVersion.Size = New Size(50, 17)
LabelVersion.Size = New Size(45, 15)
LabelVersion.TabIndex = 5
LabelVersion.Text = "Version"
'
' LinkLabelReadtheWiki
'
LinkLabelReadtheWiki.AutoSize = True
LinkLabelReadtheWiki.Font = New Font("Comic Sans MS", 9F, FontStyle.Regular, GraphicsUnit.Point)
LinkLabelReadtheWiki.Location = New Point(337, 54)
LinkLabelReadtheWiki.Font = New Font("Segoe UI", 9F, FontStyle.Regular, GraphicsUnit.Point)
LinkLabelReadtheWiki.Location = New Point(313, 43)
LinkLabelReadtheWiki.Name = "LinkLabelReadtheWiki"
LinkLabelReadtheWiki.Size = New Size(85, 17)
LinkLabelReadtheWiki.Size = New Size(79, 15)
LinkLabelReadtheWiki.TabIndex = 6
LinkLabelReadtheWiki.TabStop = True
LinkLabelReadtheWiki.Text = "Read the Wiki"
@@ -96,18 +96,18 @@ Partial Class AboutBox
Panel1.Controls.Add(LabelProgramName)
Panel1.Controls.Add(LinkLabelReadtheWiki)
Panel1.Controls.Add(LabelVersion)
Panel1.Location = New Point(117, 12)
Panel1.Location = New Point(106, 12)
Panel1.Name = "Panel1"
Panel1.Size = New Size(440, 111)
Panel1.Size = New Size(409, 93)
Panel1.TabIndex = 7
'
' LicenseRichTextBox
'
LicenseRichTextBox.Font = New Font("Cambria", 9.75F, FontStyle.Regular, GraphicsUnit.Point)
LicenseRichTextBox.Location = New Point(12, 135)
LicenseRichTextBox.Location = New Point(12, 113)
LicenseRichTextBox.Name = "LicenseRichTextBox"
LicenseRichTextBox.ReadOnly = True
LicenseRichTextBox.Size = New Size(545, 305)
LicenseRichTextBox.Size = New Size(503, 329)
LicenseRichTextBox.TabIndex = 1
LicenseRichTextBox.Text = "License notice"
'
@@ -116,7 +116,7 @@ Partial Class AboutBox
AutoScaleDimensions = New SizeF(7F, 15F)
AutoScaleMode = AutoScaleMode.Font
BackColor = Color.Gainsboro
ClientSize = New Size(569, 454)
ClientSize = New Size(526, 453)
Controls.Add(LicenseRichTextBox)
Controls.Add(Panel1)
Controls.Add(PictureBoxLogo)

View File

@@ -21,8 +21,8 @@ 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."
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."
''' <summary>
''' Handles the Load event for the AboutBox form.
@@ -33,10 +33,10 @@ SOFTWARE."
settings.LoadSettings()
settings.ToggleDarkMode(settings.DarkMode)
LabelProgramName.Text = My.Application.Info.AssemblyName
LabelProgramName.Text = My.Application.Info.ProductName
LabelProgramDescription.Text = "Given a subreddit name and a keyword,
RPST returns all top posts
(by default) that contain the specified keyword."
RPST returns all top posts (by default)
that contain the specified keyword."
LabelVersion.Text = $"v{My.Application.Info.Version}"
LicenseRichTextBox.Text = LicenseText
End Sub

View File

@@ -7,31 +7,31 @@ Public Class DataGridViewHandler
''' <param name="dataGridView">The DataGridView to be initialized.</param>
Public Shared Sub AddColumn(dataGridView As DataGridView)
' Clear the Columns and Rows before adding Items to them
PostsForm.DataGridViewPosts.Rows.Clear()
PostsForm.DataGridViewPosts.Columns.Clear()
dataGridView.Rows.Clear()
dataGridView.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")
dataGridView.Columns.Add("PostCount", "INDEX")
dataGridView.Columns.Add("PostAuthor", "AUTHOR")
dataGridView.Columns.Add("PostID", "ID")
dataGridView.Columns.Add("PostText", "TEXT")
dataGridView.Columns.Add("PostSubreddit", "SUBREDDIT")
dataGridView.Columns.Add("SubredditVisibility", "VISIBILITY")
dataGridView.Columns.Add("PostThumbnail", "THUMBNAIL")
dataGridView.Columns.Add("PostIsNSFW", "NSFW")
dataGridView.Columns.Add("PostIsGilded", "GILDED")
dataGridView.Columns.Add("PostUpvotes", "UPVOTES")
dataGridView.Columns.Add("PostUpvoteRatio", "UPVOTE RATIO")
dataGridView.Columns.Add("PostDownvotes", "DOWNVOTES")
dataGridView.Columns.Add("PostAwards", "AWARDS")
dataGridView.Columns.Add("PostTopAward", "TOP AWARD")
dataGridView.Columns.Add("PostIsCrosspostable", "IS CROSS-POSTABLE?")
dataGridView.Columns.Add("PostScore", "SCORE")
dataGridView.Columns.Add("PostCategory", "CATEGORY")
dataGridView.Columns.Add("PostDomain", "DOMAIN")
dataGridView.Columns.Add("PostPermalink", "PERMALINK")
dataGridView.Columns.Add("PostCreatedAt", "CREATED AT")
dataGridView.Columns.Add("PostApprovedAt", "APPROVED ATt")
dataGridView.Columns.Add("PostApprovedBy", "APPROVED BY")
End Sub
Public Shared Sub AddRow(dataGridView As DataGridView, post As JObject, postNumber As Integer)
@@ -41,9 +41,10 @@ Public Class DataGridViewHandler
''' <param name="dataGridView">The DataGridView to which the row will be added.</param>
''' <param name="post">A JObject representing the Reddit post.</param>
''' <param name="postNumber">The number of the post.</param>
PostsForm.DataGridViewPosts.Rows.Add(postNumber,
dataGridView.Rows.Add(postNumber,
post("data")("author"),
post("data")("id"),
post("data")("selftext"),
post("data")("subreddit_name_prefixed"),
post("data")("subreddit_type"),
post("data")("thumbnail"),
@@ -56,7 +57,6 @@ Public Class DataGridViewHandler
post("data")("top_awarded_type"),
post("data")("is_crosspostable"),
post("data")("score"),
post("data")("selftext"),
post("data")("category"),
post("data")("domain"),
post("data")("permalink"),

View File

@@ -1,5 +1,5 @@
<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()>
Partial Class DeveloperForm
Partial Class DeveloperBox
Inherits System.Windows.Forms.Form
'Form overrides dispose to clean up the component list.
@@ -22,7 +22,7 @@ Partial Class DeveloperForm
'Do not modify it using the code editor.
<System.Diagnostics.DebuggerStepThrough()>
Private Sub InitializeComponent()
Dim resources As ComponentModel.ComponentResourceManager = New ComponentModel.ComponentResourceManager(GetType(DeveloperForm))
Dim resources As ComponentModel.ComponentResourceManager = New ComponentModel.ComponentResourceManager(GetType(DeveloperBox))
AboutMeLinkLabel = New LinkLabel()
LinkLabelBuyMeACoffee = New LinkLabel()
GreetingLabel = New Label()

View File

@@ -1,4 +1,4 @@
Public Class DeveloperForm
Public Class DeveloperBox
Private Sub DeveloperForm_Load(sender As Object, e As EventArgs) Handles MyBase.Load
GreetingLabel.BackColor = Color.Transparent
AboutMeLinkLabel.BackColor = Color.Transparent

311
RPST GUI/RPST/FormMain.Designer.vb generated Normal file
View File

@@ -0,0 +1,311 @@
<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()>
Partial Class FormMain
Inherits System.Windows.Forms.Form
'Form overrides dispose to clean up the component list.
<System.Diagnostics.DebuggerNonUserCode()>
Protected Overrides Sub Dispose(ByVal disposing As Boolean)
Try
If disposing AndAlso components IsNot Nothing Then
components.Dispose()
End If
Finally
MyBase.Dispose(disposing)
End Try
End Sub
'Required by the Windows Form Designer
Private components As System.ComponentModel.IContainer
'NOTE: The following procedure is required by the Windows Form Designer
'It can be modified using the Windows Form Designer.
'Do not modify it using the code editor.
<System.Diagnostics.DebuggerStepThrough()>
Private Sub InitializeComponent()
components = New ComponentModel.Container()
Dim resources As ComponentModel.ComponentResourceManager = New ComponentModel.ComponentResourceManager(GetType(FormMain))
TextBoxKeyword = New TextBox()
TextBoxSubreddit = New TextBox()
ButtonScrape = New Button()
ComboBoxTimeframe = New ComboBox()
ComboBoxListing = New ComboBox()
LabelKeyword = New Label()
LabelSubreddit = New Label()
LabelLimit = New Label()
LabelListing = New Label()
LabelTimeframe = New Label()
ContextMenuStripRightClick = New ContextMenuStrip(components)
ToolStripMenuItemDarkMode = New ToolStripMenuItem()
ToolStripMenuItemSavePosts = New ToolStripMenuItem()
ToolStripMenuItemtoJSON = New ToolStripMenuItem()
ToolStripMenuItemtoCSV = New ToolStripMenuItem()
ToolStripMenuItemAbout = New ToolStripMenuItem()
ToolStripMenuItemDeveloper = New ToolStripMenuItem()
ToolStripMenuItemCheckUpdates = New ToolStripMenuItem()
ToolStripMenuItemQuit = New ToolStripMenuItem()
NumericUpDownLimit = New NumericUpDown()
ToolTip = New ToolTip(components)
ContextMenuStripRightClick.SuspendLayout()
CType(NumericUpDownLimit, ComponentModel.ISupportInitialize).BeginInit()
SuspendLayout()
'
' TextBoxKeyword
'
TextBoxKeyword.BackColor = SystemColors.Window
TextBoxKeyword.ForeColor = SystemColors.WindowText
TextBoxKeyword.Location = New Point(118, 20)
TextBoxKeyword.Name = "TextBoxKeyword"
TextBoxKeyword.PlaceholderText = "Keyword"
TextBoxKeyword.Size = New Size(100, 23)
TextBoxKeyword.TabIndex = 0
ToolTip.SetToolTip(TextBoxKeyword, "Enter the keyword you want to search for.")
'
' TextBoxSubreddit
'
TextBoxSubreddit.Location = New Point(118, 49)
TextBoxSubreddit.Name = "TextBoxSubreddit"
TextBoxSubreddit.PlaceholderText = "Subreddit"
TextBoxSubreddit.Size = New Size(100, 23)
TextBoxSubreddit.TabIndex = 4
ToolTip.SetToolTip(TextBoxSubreddit, "Provide the subreddit to search in.")
'
' ButtonScrape
'
ButtonScrape.Location = New Point(167, 174)
ButtonScrape.Name = "ButtonScrape"
ButtonScrape.Size = New Size(51, 28)
ButtonScrape.TabIndex = 6
ButtonScrape.Text = "Scrape"
ToolTip.SetToolTip(ButtonScrape, "You can also just hit ENTER to start scraping.")
ButtonScrape.UseVisualStyleBackColor = True
'
' ComboBoxTimeframe
'
ComboBoxTimeframe.AutoCompleteCustomSource.AddRange(New String() {"Hour", "Day", "Week", "Month", "Year"})
ComboBoxTimeframe.AutoCompleteMode = AutoCompleteMode.Append
ComboBoxTimeframe.AutoCompleteSource = AutoCompleteSource.CustomSource
ComboBoxTimeframe.FormattingEnabled = True
ComboBoxTimeframe.Items.AddRange(New Object() {"Hour", "Day", "Week", "Month", "Year"})
ComboBoxTimeframe.Location = New Point(118, 136)
ComboBoxTimeframe.Name = "ComboBoxTimeframe"
ComboBoxTimeframe.Size = New Size(100, 23)
ComboBoxTimeframe.TabIndex = 8
ComboBoxTimeframe.Text = "All"
ToolTip.SetToolTip(ComboBoxTimeframe, "Select the time period for the posts. Default value is `All`.")
'
' ComboBoxListing
'
ComboBoxListing.AutoCompleteCustomSource.AddRange(New String() {"Controversial", "Hot", "Best", "New", "Rising"})
ComboBoxListing.AutoCompleteMode = AutoCompleteMode.Append
ComboBoxListing.AutoCompleteSource = AutoCompleteSource.CustomSource
ComboBoxListing.FormattingEnabled = True
ComboBoxListing.Items.AddRange(New Object() {"Controversial", "Hot", "Best", "New", "Rising"})
ComboBoxListing.Location = New Point(118, 107)
ComboBoxListing.Name = "ComboBoxListing"
ComboBoxListing.Size = New Size(100, 23)
ComboBoxListing.TabIndex = 9
ComboBoxListing.Text = "Top"
ToolTip.SetToolTip(ComboBoxListing, "Choose the type of post listings. Default value is `Top`.")
'
' LabelKeyword
'
LabelKeyword.AutoEllipsis = True
LabelKeyword.Font = New Font("Segoe UI Semibold", 9F, FontStyle.Bold Or FontStyle.Underline, GraphicsUnit.Point)
LabelKeyword.ForeColor = Color.Black
LabelKeyword.Location = New Point(19, 23)
LabelKeyword.Name = "LabelKeyword"
LabelKeyword.Size = New Size(71, 20)
LabelKeyword.TabIndex = 10
LabelKeyword.Text = "Keyword:"
'
' LabelSubreddit
'
LabelSubreddit.AutoEllipsis = True
LabelSubreddit.Font = New Font("Segoe UI Semibold", 9F, FontStyle.Bold Or FontStyle.Underline, GraphicsUnit.Point)
LabelSubreddit.ForeColor = Color.Black
LabelSubreddit.Location = New Point(19, 52)
LabelSubreddit.Name = "LabelSubreddit"
LabelSubreddit.Size = New Size(71, 23)
LabelSubreddit.TabIndex = 11
LabelSubreddit.Text = "Subreddit:"
'
' LabelLimit
'
LabelLimit.AutoEllipsis = True
LabelLimit.Font = New Font("Segoe UI Semibold", 9F, FontStyle.Bold Or FontStyle.Underline, GraphicsUnit.Point)
LabelLimit.ForeColor = Color.Black
LabelLimit.Location = New Point(19, 75)
LabelLimit.Name = "LabelLimit"
LabelLimit.Size = New Size(56, 23)
LabelLimit.TabIndex = 12
LabelLimit.Text = "Limit:"
'
' LabelListing
'
LabelListing.AutoEllipsis = True
LabelListing.Font = New Font("Segoe UI Semibold", 9F, FontStyle.Bold Or FontStyle.Underline, GraphicsUnit.Point)
LabelListing.ForeColor = Color.Black
LabelListing.Location = New Point(19, 107)
LabelListing.Name = "LabelListing"
LabelListing.Size = New Size(56, 23)
LabelListing.TabIndex = 13
LabelListing.Text = "Listing:"
'
' LabelTimeframe
'
LabelTimeframe.AutoEllipsis = True
LabelTimeframe.Font = New Font("Segoe UI Semibold", 9F, FontStyle.Bold Or FontStyle.Underline, GraphicsUnit.Point)
LabelTimeframe.ForeColor = Color.Black
LabelTimeframe.Location = New Point(19, 136)
LabelTimeframe.Name = "LabelTimeframe"
LabelTimeframe.Size = New Size(81, 23)
LabelTimeframe.TabIndex = 14
LabelTimeframe.Text = "Timeframe:"
'
' ContextMenuStripRightClick
'
ContextMenuStripRightClick.Items.AddRange(New ToolStripItem() {ToolStripMenuItemDarkMode, ToolStripMenuItemSavePosts, ToolStripMenuItemAbout, ToolStripMenuItemDeveloper, ToolStripMenuItemCheckUpdates, ToolStripMenuItemQuit})
ContextMenuStripRightClick.Name = "ContextMenuStrip1"
ContextMenuStripRightClick.Size = New Size(154, 136)
'
' ToolStripMenuItemDarkMode
'
ToolStripMenuItemDarkMode.AutoToolTip = True
ToolStripMenuItemDarkMode.CheckOnClick = True
ToolStripMenuItemDarkMode.Image = CType(resources.GetObject("ToolStripMenuItemDarkMode.Image"), Image)
ToolStripMenuItemDarkMode.Name = "ToolStripMenuItemDarkMode"
ToolStripMenuItemDarkMode.Size = New Size(153, 22)
ToolStripMenuItemDarkMode.Text = "Dark Mode"
'
' ToolStripMenuItemSavePosts
'
ToolStripMenuItemSavePosts.AutoToolTip = True
ToolStripMenuItemSavePosts.DropDownItems.AddRange(New ToolStripItem() {ToolStripMenuItemtoJSON, ToolStripMenuItemtoCSV})
ToolStripMenuItemSavePosts.Image = CType(resources.GetObject("ToolStripMenuItemSavePosts.Image"), Image)
ToolStripMenuItemSavePosts.Name = "ToolStripMenuItemSavePosts"
ToolStripMenuItemSavePosts.Size = New Size(153, 22)
ToolStripMenuItemSavePosts.Text = "Save Posts"
ToolStripMenuItemSavePosts.ToolTipText = "Save found posts to..."
'
' ToolStripMenuItemtoJSON
'
ToolStripMenuItemtoJSON.AutoToolTip = True
ToolStripMenuItemtoJSON.CheckOnClick = True
ToolStripMenuItemtoJSON.Image = CType(resources.GetObject("ToolStripMenuItemtoJSON.Image"), Image)
ToolStripMenuItemtoJSON.Name = "ToolStripMenuItemtoJSON"
ToolStripMenuItemtoJSON.Size = New Size(116, 22)
ToolStripMenuItemtoJSON.Text = "to JSON"
'
' ToolStripMenuItemtoCSV
'
ToolStripMenuItemtoCSV.AutoToolTip = True
ToolStripMenuItemtoCSV.Enabled = False
ToolStripMenuItemtoCSV.Image = CType(resources.GetObject("ToolStripMenuItemtoCSV.Image"), Image)
ToolStripMenuItemtoCSV.Name = "ToolStripMenuItemtoCSV"
ToolStripMenuItemtoCSV.Size = New Size(116, 22)
ToolStripMenuItemtoCSV.Text = "to CSV"
'
' ToolStripMenuItemAbout
'
ToolStripMenuItemAbout.AutoToolTip = True
ToolStripMenuItemAbout.Image = CType(resources.GetObject("ToolStripMenuItemAbout.Image"), Image)
ToolStripMenuItemAbout.Name = "ToolStripMenuItemAbout"
ToolStripMenuItemAbout.Size = New Size(153, 22)
ToolStripMenuItemAbout.Text = "About"
'
' ToolStripMenuItemDeveloper
'
ToolStripMenuItemDeveloper.AutoToolTip = True
ToolStripMenuItemDeveloper.Image = CType(resources.GetObject("ToolStripMenuItemDeveloper.Image"), Image)
ToolStripMenuItemDeveloper.Name = "ToolStripMenuItemDeveloper"
ToolStripMenuItemDeveloper.Size = New Size(153, 22)
ToolStripMenuItemDeveloper.Text = "Developer"
'
' ToolStripMenuItemCheckUpdates
'
ToolStripMenuItemCheckUpdates.AutoToolTip = True
ToolStripMenuItemCheckUpdates.Image = CType(resources.GetObject("ToolStripMenuItemCheckUpdates.Image"), Image)
ToolStripMenuItemCheckUpdates.Name = "ToolStripMenuItemCheckUpdates"
ToolStripMenuItemCheckUpdates.Size = New Size(153, 22)
ToolStripMenuItemCheckUpdates.Text = "Check Updates"
'
' ToolStripMenuItemQuit
'
ToolStripMenuItemQuit.AutoToolTip = True
ToolStripMenuItemQuit.Image = CType(resources.GetObject("ToolStripMenuItemQuit.Image"), Image)
ToolStripMenuItemQuit.Name = "ToolStripMenuItemQuit"
ToolStripMenuItemQuit.Size = New Size(153, 22)
ToolStripMenuItemQuit.Text = "Quit"
'
' NumericUpDownLimit
'
NumericUpDownLimit.Location = New Point(118, 78)
NumericUpDownLimit.Minimum = New Decimal(New Integer() {5, 0, 0, 0})
NumericUpDownLimit.Name = "NumericUpDownLimit"
NumericUpDownLimit.ReadOnly = True
NumericUpDownLimit.Size = New Size(100, 23)
NumericUpDownLimit.TabIndex = 15
ToolTip.SetToolTip(NumericUpDownLimit, "Set how many posts you want to go through. Default value is `10`.")
NumericUpDownLimit.Value = New Decimal(New Integer() {10, 0, 0, 0})
'
' ToolTip
'
ToolTip.AutoPopDelay = 5000
ToolTip.BackColor = Color.Gainsboro
ToolTip.InitialDelay = 500
ToolTip.ReshowDelay = 100
ToolTip.ToolTipIcon = ToolTipIcon.Info
ToolTip.ToolTipTitle = "Tip"
'
' FormMain
'
AutoScaleDimensions = New SizeF(7F, 15F)
AutoScaleMode = AutoScaleMode.Font
BackColor = SystemColors.Control
ClientSize = New Size(239, 221)
ContextMenuStrip = ContextMenuStripRightClick
Controls.Add(ComboBoxTimeframe)
Controls.Add(TextBoxKeyword)
Controls.Add(LabelTimeframe)
Controls.Add(LabelKeyword)
Controls.Add(ComboBoxListing)
Controls.Add(NumericUpDownLimit)
Controls.Add(LabelListing)
Controls.Add(ButtonScrape)
Controls.Add(LabelLimit)
Controls.Add(LabelSubreddit)
Controls.Add(TextBoxSubreddit)
FormBorderStyle = FormBorderStyle.FixedSingle
Icon = CType(resources.GetObject("$this.Icon"), Icon)
MaximizeBox = False
Name = "FormMain"
StartPosition = FormStartPosition.CenterScreen
Text = "RPST"
ContextMenuStripRightClick.ResumeLayout(False)
CType(NumericUpDownLimit, ComponentModel.ISupportInitialize).EndInit()
ResumeLayout(False)
PerformLayout()
End Sub
Friend WithEvents TextBoxKeyword As TextBox
Friend WithEvents TextBoxSubreddit As TextBox
Friend WithEvents ButtonScrape As Button
Friend WithEvents ComboBoxTimeframe As ComboBox
Friend WithEvents ComboBoxListing As ComboBox
Friend WithEvents LabelKeyword As Label
Friend WithEvents LabelSubreddit As Label
Friend WithEvents LabelLimit As Label
Friend WithEvents LabelListing As Label
Friend WithEvents LabelTimeframe As Label
Friend WithEvents ContextMenuStripRightClick As ContextMenuStrip
Friend WithEvents ToolStripMenuItemSavePosts As ToolStripMenuItem
Friend WithEvents ToolStripMenuItemtoJSON As ToolStripMenuItem
Friend WithEvents ToolStripMenuItemtoCSV As ToolStripMenuItem
Friend WithEvents NumericUpDownLimit As NumericUpDown
Friend WithEvents ToolStripMenuItemDarkMode As ToolStripMenuItem
Friend WithEvents ToolStripMenuItemAbout As ToolStripMenuItem
Friend WithEvents ToolStripMenuItemDeveloper As ToolStripMenuItem
Friend WithEvents ToolStripMenuItemCheckUpdates As ToolStripMenuItem
Friend WithEvents ToolStripMenuItemQuit As ToolStripMenuItem
Friend WithEvents ToolTip As ToolTip
End Class

File diff suppressed because it is too large Load Diff

184
RPST GUI/RPST/FormMain.vb Normal file
View File

@@ -0,0 +1,184 @@
Imports System.IO
Imports System.Windows.Forms.VisualStyles.VisualStyleElement
Imports Newtonsoft.Json
Imports Newtonsoft.Json.Linq
Public Class FormMain
ReadOnly settings As New SettingsManager()
ReadOnly ApiHandler As New ApiHandler()
''' <summary>
''' Event handler for the form load event.
''' It loads settings, toggles dark mode if necessary, checks for directories, logs first time launch, and sets the form title.
''' </summary>
''' <param name="sender">The source of the event.</param>
''' <param name="e">An EventArgs that contains the event data.</param>
Private Sub FormMain_Load(sender As Object, e As EventArgs) Handles MyBase.Load
settings.LoadSettings()
settings.ToggleDarkMode(settings.DarkMode)
Utilities.PathFinder()
Utilities.LogFirstTimeLaunch()
Me.Text = My.Application.Info.AssemblyName
End Sub
''' <summary>
''' Event handler for the 'Dark Mode' checkbox change event.
''' It toggles the dark mode of the application based on the checkbox status.
''' </summary>
''' <param name="sender">The source of the event.</param>
''' <param name="e">An EventArgs that contains the event data.</param>
Private Sub DarkModeToolStripMenuItem_CheckedChanged(sender As Object, e As EventArgs) Handles ToolStripMenuItemDarkMode.CheckedChanged
settings.ToggleDarkMode(ToolStripMenuItemDarkMode.Checked)
End Sub
''' <summary>
''' Event handler for the 'About' menu item click.
''' It shows the 'About' box.
''' </summary>
''' <param name="sender">The source of the event.</param>
''' <param name="e">An EventArgs that contains the event data.</param>
Private Sub ToolStripMenuItemAbout_Click(sender As Object, e As EventArgs) Handles ToolStripMenuItemAbout.Click
AboutBox.Show()
End Sub
''' <summary>
''' Event handler for the 'Developer' menu item click.
''' It shows the 'Developer' dialog box.
''' </summary>
''' <param name="sender">The source of the event.</param>
''' <param name="e">An EventArgs that contains the event data.</param>
Private Sub ToolStripMenuItemDeveloper_Click(sender As Object, e As EventArgs) Handles ToolStripMenuItemDeveloper.Click
DeveloperBox.ShowDialog()
End Sub
''' <summary>
''' Event handler for the 'Check Updates' menu item click.
''' It checks for application updates and provides update information if a newer version is available.
''' </summary>
''' <param name="sender">The source of the event.</param>
''' <param name="e">An EventArgs that contains the event data.</param>
Private Sub ToolStripMenuItemCheckUpdates_Click(sender As Object, e As EventArgs) Handles ToolStripMenuItemCheckUpdates.Click
Dim data As JObject = ApiHandler.CheckUpdates()
If data("tag_name").ToString = My.Application.Info.Version.ToString Then
MessageBox.Show($"You're running the latest version v{My.Application.Info.Version} of {Me.Text}. Check again soon! :)", $"{Me.Text} v{My.Application.Info.Version}", MessageBoxButtons.OK, MessageBoxIcon.Information)
Else
Dim confirm As DialogResult = MessageBox.Show($"A new version v{data("tag_name")} of {Me.Text} is available, would you like to get it?
{data("body")}
", $"{Me.Text} v{data("tag_name")}".ToString, MessageBoxButtons.YesNo, MessageBoxIcon.Question)
If confirm = DialogResult.Yes Then
Shell($"cmd /c start {data("html_url")}")
End If
End If
End Sub
''' <summary>
''' Event handler for the 'Quit' menu item click.
''' It asks the user for confirmation and closes the program if the user agrees.
''' </summary>
''' <param name="sender">The source of the event.</param>
''' <param name="e">An EventArgs that contains the event data.</param>
Private Sub ToolStripMenuItemQuit_Click(sender As Object, e As EventArgs) Handles ToolStripMenuItemQuit.Click
Dim result As DialogResult = MessageBox.Show("This will close the program, continue?", "Quit", MessageBoxButtons.YesNo, MessageBoxIcon.Question)
If result = DialogResult.Yes Then
Me.Close()
End If
End Sub
''' <summary>
''' Handles the click event of the ScrapeButton.
''' Collects inputs, fetches Reddit posts based on the inputs,
''' and processes Reddit posts.
''' </summary>
''' <param name="sender">The sender of the event.</param>
''' <param name="e">The EventArgs instance containing the event data.</param>
Private Sub ButtonScrape_Click(sender As Object, e As EventArgs) Handles ButtonScrape.Click
Utilities.ProcessRedditPosts(ToolStripMenuItemtoJSON)
End Sub
''' <summary>
''' Handles the KeyDown event for the TextBoxKeyword.
''' Processes Reddit posts when the Enter key is pressed.
''' </summary>
''' <param name="sender">The source of the event.</param>
''' <param name="e">The <see cref="KeyEventArgs"/> instance containing the event data.</param>
Private Sub TextBoxKeyword_KeyDown(sender As Object, e As KeyEventArgs) Handles TextBoxKeyword.KeyDown
' Check if the Enter key is pressed
If e.KeyCode = Keys.Enter Then
' Prevent the beep sound that usually comes with the Enter key in a single-line TextBox
e.SuppressKeyPress = True
Utilities.ProcessRedditPosts(ToolStripMenuItemtoJSON)
End If
End Sub
''' <summary>
''' Handles the KeyDown event for the TextBoxSubreddit.
''' Processes Reddit posts when the Enter key is pressed.
''' </summary>
''' <param name="sender">The source of the event.</param>
''' <param name="e">The <see cref="KeyEventArgs"/> instance containing the event data.</param>
Private Sub TextBoxSubreddit_KeyDown(sender As Object, e As KeyEventArgs) Handles TextBoxSubreddit.KeyDown
' Check if the Enter key is pressed
If e.KeyCode = Keys.Enter Then
' Prevent the beep sound that usually comes with the Enter key in a single-line TextBox
e.SuppressKeyPress = True
Utilities.ProcessRedditPosts(ToolStripMenuItemtoJSON)
End If
End Sub
''' <summary>
''' Handles the KeyDown event for the NumericUpDownLimit.
''' Processes Reddit posts when the Enter key is pressed.
''' </summary>
''' <param name="sender">The source of the event.</param>
''' <param name="e">The <see cref="KeyEventArgs"/> instance containing the event data.</param>
Private Sub NumericUpDownLimit_KeyDown(sender As Object, e As KeyEventArgs) Handles NumericUpDownLimit.KeyDown
' Check if the Enter key is pressed
If e.KeyCode = Keys.Enter Then
' Prevent the beep sound that usually comes with the Enter key in a single-line TextBox
e.SuppressKeyPress = True
Utilities.ProcessRedditPosts(ToolStripMenuItemtoJSON)
End If
End Sub
''' <summary>
''' Handles the KeyDown event for the ComboBoxListing.
''' Processes Reddit posts when the Enter key is pressed.
''' </summary>
''' <param name="sender">The source of the event.</param>
''' <param name="e">The <see cref="KeyEventArgs"/> instance containing the event data.</param>
Private Sub ComboBoxListing_KeyDown(sender As Object, e As KeyEventArgs) Handles ComboBoxListing.KeyDown
' Check if the Enter key is pressed
If e.KeyCode = Keys.Enter Then
' Prevent the beep sound that usually comes with the Enter key in a single-line TextBox
e.SuppressKeyPress = True
Utilities.ProcessRedditPosts(ToolStripMenuItemtoJSON)
End If
End Sub
''' <summary>
''' Handles the KeyDown event for the ComboBoxTimeframe.
''' Processes Reddit posts when the Enter key is pressed.
''' </summary>
''' <param name="sender">The source of the event.</param>
''' <param name="e">The <see cref="KeyEventArgs"/> instance containing the event data.</param>
Private Sub ComboBoxTimeframe_KeyDown(sender As Object, e As KeyEventArgs) Handles ComboBoxTimeframe.KeyDown
' Check if the Enter key is pressed
If e.KeyCode = Keys.Enter Then
' Prevent the beep sound that usually comes with the Enter key in a single-line TextBox
e.SuppressKeyPress = True
Utilities.ProcessRedditPosts(ToolStripMenuItemtoJSON)
End If
End Sub
End Class

View File

@@ -1,9 +1,9 @@
<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()> _
Partial Class PostsForm
<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()>
Partial Class FormPosts
Inherits System.Windows.Forms.Form
'Form overrides dispose to clean up the component list.
<System.Diagnostics.DebuggerNonUserCode()> _
<System.Diagnostics.DebuggerNonUserCode()>
Protected Overrides Sub Dispose(ByVal disposing As Boolean)
Try
If disposing AndAlso components IsNot Nothing Then
@@ -20,8 +20,9 @@ Partial Class PostsForm
'NOTE: The following procedure is required by the Windows Form Designer
'It can be modified using the Windows Form Designer.
'Do not modify it using the code editor.
<System.Diagnostics.DebuggerStepThrough()> _
<System.Diagnostics.DebuggerStepThrough()>
Private Sub InitializeComponent()
Dim resources As ComponentModel.ComponentResourceManager = New ComponentModel.ComponentResourceManager(GetType(FormPosts))
DataGridViewPosts = New DataGridView()
CType(DataGridViewPosts, ComponentModel.ISupportInitialize).BeginInit()
SuspendLayout()
@@ -36,19 +37,20 @@ Partial Class PostsForm
DataGridViewPosts.ReadOnly = True
DataGridViewPosts.RowHeadersVisible = False
DataGridViewPosts.RowTemplate.Height = 25
DataGridViewPosts.Size = New Size(800, 450)
DataGridViewPosts.Size = New Size(501, 365)
DataGridViewPosts.TabIndex = 3
'
' PostsForm
' FormPosts
'
AutoScaleDimensions = New SizeF(7F, 15F)
AutoScaleMode = AutoScaleMode.Font
ClientSize = New Size(800, 450)
ClientSize = New Size(501, 365)
Controls.Add(DataGridViewPosts)
Name = "PostsForm"
ShowIcon = False
Icon = CType(resources.GetObject("$this.Icon"), Icon)
Name = "FormPosts"
ShowInTaskbar = False
Text = "PostsForm"
StartPosition = FormStartPosition.CenterScreen
Text = "Posts"
CType(DataGridViewPosts, ComponentModel.ISupportInitialize).EndInit()
ResumeLayout(False)
End Sub

View File

@@ -0,0 +1,393 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing"">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
<data name="$this.Icon" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
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
</value>
</data>
</root>

View File

@@ -0,0 +1,5 @@
Public Class FormPosts
Private Sub FormResults_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Me.Text = $"{Me.Text} - {FormMain.TextBoxKeyword.Text}, r/{FormMain.TextBoxSubreddit.Text}, {FormMain.NumericUpDownLimit.Text}, {FormMain.ComboBoxListing.Text}, {FormMain.ComboBoxTimeframe.Text}"
End Sub
End Class

View File

@@ -33,7 +33,7 @@ Namespace My
<Global.System.Diagnostics.DebuggerStepThroughAttribute()> _
Protected Overrides Sub OnCreateMainForm()
Me.MainForm = Global.RPST.StartForm
Me.MainForm = Global.RPST.FormMain
End Sub
End Class
End Namespace

View File

@@ -1,60 +0,0 @@
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -1,3 +0,0 @@
Public Class PostsForm
End Class

View File

@@ -1,7 +1,7 @@
Imports Newtonsoft.Json.Linq
Public Class PostsProcessor
Private ApiHandler As New ApiHandler
Private ReadOnly ApiHandler As New ApiHandler
''' <summary>
''' Fetches Reddit posts based on the given parameters and returns them as a JObject.
@@ -23,7 +23,7 @@ Public Class PostsProcessor
''' <param name="keyword">The keyword to check for.</param>
''' <returns>True if the post contains the keyword, False otherwise.</returns>
Public Shared Function PostContainsKeyword(post As JObject, keyword As String) As Boolean
Return post("data")("selftext").ToString.ToLower(System.Globalization.CultureInfo.InvariantCulture).Contains(keyword.ToLower(System.Globalization.CultureInfo.InvariantCulture))
Return post("data")("selftext").ToString.ToLower(Globalization.CultureInfo.InvariantCulture).Contains(keyword.ToLower(System.Globalization.CultureInfo.InvariantCulture))
End Function
End Class

View File

@@ -1,23 +1,18 @@
# RPST (Reddit Post Scraping Tool)
Given a subreddit name and a keyword, this script will return all posts from a specified listing (default is 'top') that contain the provided keyword.
[![Upload Python Package](https://github.com/rly0nheart/reddit-post-scraping-tool/actions/workflows/python-publish.yml/badge.svg)](https://github.com/rly0nheart/reddit-post-scraping-tool/actions/workflows/python-publish.yml) [![CodeQL](https://github.com/rly0nheart/reddit-post-scraping-tool/actions/workflows/codeql.yml/badge.svg)](https://github.com/rly0nheart/reddit-post-scraping-tool/actions/workflows/codeql.yml) ![.Net](https://img.shields.io/badge/.NET-5C2D91?style=flat&logo=.net&logoColor=white) ![Python](https://img.shields.io/badge/python-3670A0?style=flat&logo=python&logoColor=ffdd54)
![2023-08-07_02-13_1](https://github.com/bellingcat/reddit-post-scraping-tool/assets/74001397/5ea98745-8b5f-4a93-9a53-befa491f7b6a)
![2023-08-07_02-13](https://github.com/bellingcat/reddit-post-scraping-tool/assets/74001397/f303abc7-8a83-44b0-97c9-a447c459cef9)
Given a subreddit name and a keyword, RPST will return all posts from a specified listing (default is 'top') that contain the provided keyword.
[![Upload Python Package](https://github.com/bellingcat/reddit-post-scraping-tool/actions/workflows/python-publish.yml/badge.svg)](https://github.com/rly0nheart/reddit-post-scraping-tool/actions/workflows/python-publish.yml) [![CodeQL](https://github.com/bellingcat/reddit-post-scraping-tool/actions/workflows/codeql.yml/badge.svg)](https://github.com/rly0nheart/reddit-post-scraping-tool/actions/workflows/codeql.yml) ![.Net](https://img.shields.io/badge/.NET-5C2D91?style=flat&logo=.net&logoColor=white) ![Python](https://img.shields.io/badge/python-3670A0?style=flat&logo=python&logoColor=ffdd54)
# ✅ Features
## GUI
- [x] Dark mode (Right-click)
- [x] Saves results to a JSON (Right-click)
- [x] Dark mode (*Right-click*)
- [x] Saves results to a JSON file (*Right-click*)
- [x] Logs errors to a file
## CLI
- [x] Saves results to a JSON (-j/--json)
- [x] Automatically checks for new updates. Notifies user if update were found.
- [x] Saves results to JSON (*specifiy* `--json`)
- [x] Saves results to CSV (*specify* `--csv`)
- [x] Automatically checks for new updates, and notifies user if updates were found.
# 📃 TODO
## GUI
@@ -25,8 +20,21 @@ Given a subreddit name and a keyword, this script will return all posts from a s
- [x] Add manual dark mode option, that will be persistent in all sessions
- [ ] Make it save results to a CSV file
# Images & Screenshots
## GUI
* ![2023-08-09_04-05](https://github.com/bellingcat/reddit-post-scraping-tool/assets/74001397/d8917a35-3eac-44ce-aa96-1f9685095254)
* ![2023-08-09_04-05_1](https://github.com/bellingcat/reddit-post-scraping-tool/assets/74001397/d2fe7269-91d4-49ad-87fb-44282c5637a7)
## CLI
* ![2023-08-25_15-39](https://github.com/bellingcat/reddit-post-scraping-tool/assets/74001397/4bca09b3-271f-452d-81a7-39c9986539f2)
* ![2023-08-25_15-30](https://github.com/bellingcat/reddit-post-scraping-tool/assets/74001397/2b39bdfa-87d0-4038-90cd-14e7d3b6a84b)
* ![2023-08-25_15-35](https://github.com/bellingcat/reddit-post-scraping-tool/assets/74001397/47ba23ad-8d32-49c5-8c16-34a903fbc581)
# 📖 Wiki
[Refer to the Wiki](https://github.com/rly0nheart/reddit-post-scraping-tool/wiki) for installation instructions, in addition to all other documentation.
[Refer to the Wiki](https://github.com/bellingcat/reddit-post-scraping-tool/wiki) for installation instructions, in addition to all other documentation.
# 😁 Donations
If you like `RPST` and would like to show support, you can Buy A Coffee for the developer using the button below

View File

@@ -9,23 +9,23 @@
<ApplicationIcon>icon.ico</ApplicationIcon>
<Company>Bellingcat</Company>
<Description>Given a subreddit name and a keyword, RPST (Reddit Post Scraping Tool) returns all top (by default) posts that contain the specified keyword. </Description>
<Copyright>Copyright (c) 2023-2024 Richard Mwewa</Copyright>
<Copyright>Copyright (c) 2023 Richard Mwewa</Copyright>
<PackageProjectUrl>https://github.com/bellingcat/reddit-post-scraping-tool</PackageProjectUrl>
<PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://github.com/bellingcat/reddit-post-scraping-tool</RepositoryUrl>
<AssemblyVersion>1.4.1.0</AssemblyVersion>
<FileVersion>1.4.1.0</FileVersion>
<AssemblyVersion>1.7.0.0</AssemblyVersion>
<FileVersion>1.7.0.0</FileVersion>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>
<Version>1.4.1</Version>
<Version>1.7.0</Version>
<PackageTags>reddit;scraper;reddit-scraper;osint</PackageTags>
<PackageReleaseNotes></PackageReleaseNotes>
<AnalysisLevel>6.0-recommended</AnalysisLevel>
<PackageId>RPST (Reddit Post Scraping Tool)</PackageId>
<PackageId>RPST</PackageId>
<Authors>Richard Mwewa</Authors>
<NeutralLanguage>en</NeutralLanguage>
<Product>$(AssemblyName)</Product>
<AssemblyName>RPST (Reddit Post Scraping Tool)</AssemblyName>
<Product>$(AssemblyName) (Reddit Post Scraping Tool)</Product>
<AssemblyName>RPST</AssemblyName>
</PropertyGroup>
<ItemGroup>
@@ -39,7 +39,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>
@@ -78,4 +78,4 @@
</None>
</ItemGroup>
</Project>
</Project>

View File

@@ -4,13 +4,13 @@
<Compile Update="AboutBox.vb">
<SubType>Form</SubType>
</Compile>
<Compile Update="DeveloperForm.vb">
<Compile Update="DeveloperBox.vb">
<SubType>Form</SubType>
</Compile>
<Compile Update="PostsForm.vb">
<Compile Update="FormMain.vb">
<SubType>Form</SubType>
</Compile>
<Compile Update="StartForm.vb">
<Compile Update="FormPosts.vb">
<SubType>Form</SubType>
</Compile>
</ItemGroup>

View File

@@ -25,7 +25,7 @@ Public Class SettingsManager
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
FormMain.ToolStripMenuItemDarkMode.Checked = settings.DarkMode
Else
' Settings file does not exist
' Create a new file with default settings 'False'
@@ -34,7 +34,7 @@ Public Class SettingsManager
File.WriteAllText(settingsFilePath, jsonOutput)
Me.DarkMode = False
StartForm.DarkModeToolStripMenuItem.Checked = False
FormMain.ToolStripMenuItemDarkMode.Checked = False
End If
End Sub
@@ -46,7 +46,7 @@ Public Class SettingsManager
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)
Dim settings As SettingsManager = JsonSerializer.Deserialize(Of SettingsManager)(json, options)
settings.DarkMode = enabled
SaveSettings(settings)
ApplyTheme()
@@ -57,7 +57,7 @@ Public Class SettingsManager
''' </summary>
''' <param name="settings">An instance of the SettingsManager containing the configurations to be saved.</param>
Private Sub SaveSettings(settings)
Dim jsonOutput = Text.Json.JsonSerializer.Serialize(settings)
Dim jsonOutput = JsonSerializer.Serialize(settings)
File.WriteAllText(settingsFilePath, jsonOutput)
End Sub
@@ -70,69 +70,125 @@ Public Class SettingsManager
Dim DarkMode As Boolean = GetDarkMode()
If DarkMode Then
' Enable dark mode for the Main form
StartForm.BackColor = ColorTranslator.FromHtml("#FF121212")
StartForm.ToolsToolStripMenuTools.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
StartForm.KeywordTextBox.BackColor = ColorTranslator.FromHtml("#FF2E2E2E")
StartForm.KeywordTextBox.ForeColor = SystemColors.Control
StartForm.SubredditTextBox.BackColor = ColorTranslator.FromHtml("#FF2E2E2E")
StartForm.SubredditTextBox.ForeColor = SystemColors.Control
StartForm.LimitNumericUpDown.BackColor = ColorTranslator.FromHtml("#FF2E2E2E")
StartForm.LimitNumericUpDown.ForeColor = SystemColors.Control
StartForm.LimitNumericUpDown.BackColor = ColorTranslator.FromHtml("#FF2E2E2E")
StartForm.LimitNumericUpDown.ForeColor = SystemColors.Control
StartForm.ListingComboBox.BackColor = ColorTranslator.FromHtml("#FF2E2E2E")
StartForm.ListingComboBox.ForeColor = SystemColors.Control
StartForm.TimeframeComboBox.BackColor = ColorTranslator.FromHtml("#FF2E2E2E")
StartForm.TimeframeComboBox.ForeColor = SystemColors.Control
StartForm.LabelKeyword.ForeColor = SystemColors.Control
StartForm.LabelSubreddit.ForeColor = SystemColors.Control
StartForm.LabelLimit.ForeColor = SystemColors.Control
StartForm.LabelListing.ForeColor = SystemColors.Control
StartForm.LabelTimeframe.ForeColor = SystemColors.Control
' Background colours (I know 'Colours'/'Colors'😆)
FormMain.BackColor = ColorTranslator.FromHtml("#FF121212")
FormMain.TextBoxSubreddit.BackColor = ColorTranslator.FromHtml("#FF2E2E2E")
FormMain.TextBoxKeyword.BackColor = ColorTranslator.FromHtml("#FF2E2E2E")
FormMain.NumericUpDownLimit.BackColor = ColorTranslator.FromHtml("#FF2E2E2E")
FormMain.NumericUpDownLimit.BackColor = ColorTranslator.FromHtml("#FF2E2E2E")
FormMain.ComboBoxListing.BackColor = ColorTranslator.FromHtml("#FF2E2E2E")
FormMain.ComboBoxTimeframe.BackColor = ColorTranslator.FromHtml("#FF2E2E2E")
' Foreground colours
FormMain.TextBoxKeyword.ForeColor = SystemColors.Control
FormMain.TextBoxSubreddit.ForeColor = SystemColors.Control
FormMain.NumericUpDownLimit.ForeColor = SystemColors.Control
FormMain.NumericUpDownLimit.ForeColor = SystemColors.Control
FormMain.ComboBoxListing.ForeColor = SystemColors.Control
FormMain.ComboBoxTimeframe.ForeColor = SystemColors.Control
FormMain.LabelKeyword.ForeColor = SystemColors.Control
FormMain.LabelSubreddit.ForeColor = SystemColors.Control
FormMain.LabelLimit.ForeColor = SystemColors.Control
FormMain.LabelListing.ForeColor = SystemColors.Control
FormMain.LabelTimeframe.ForeColor = SystemColors.Control
' Enable dark mode on 'Right Click Menu' items
' Background colours
FormMain.ToolStripMenuItemDarkMode.BackColor = ColorTranslator.FromHtml("#FF121212")
FormMain.ToolStripMenuItemSavePosts.BackColor = ColorTranslator.FromHtml("#FF121212")
FormMain.ToolStripMenuItemtoJSON.BackColor = ColorTranslator.FromHtml("#FF121212")
FormMain.ToolStripMenuItemtoCSV.BackColor = ColorTranslator.FromHtml("#FF121212")
FormMain.ToolStripMenuItemAbout.BackColor = ColorTranslator.FromHtml("#FF121212")
FormMain.ToolStripMenuItemDeveloper.BackColor = ColorTranslator.FromHtml("#FF121212")
FormMain.ToolStripMenuItemCheckUpdates.BackColor = ColorTranslator.FromHtml("#FF121212")
FormMain.ToolStripMenuItemQuit.BackColor = ColorTranslator.FromHtml("#FF121212")
' Foreground colours
FormMain.ToolStripMenuItemDarkMode.ForeColor = SystemColors.Control
FormMain.ToolStripMenuItemSavePosts.ForeColor = SystemColors.Control
FormMain.ToolStripMenuItemtoJSON.ForeColor = SystemColors.Control
FormMain.ToolStripMenuItemtoCSV.ForeColor = SystemColors.Control
FormMain.ToolStripMenuItemAbout.ForeColor = SystemColors.Control
FormMain.ToolStripMenuItemDeveloper.ForeColor = SystemColors.Control
FormMain.ToolStripMenuItemCheckUpdates.ForeColor = SystemColors.Control
FormMain.ToolStripMenuItemQuit.ForeColor = SystemColors.Control
' Enable dark mode for the About box
' Background colours
AboutBox.BackColor = ColorTranslator.FromHtml("#FF121212")
AboutBox.ForeColor = SystemColors.Control
AboutBox.LicenseRichTextBox.BackColor = ColorTranslator.FromHtml("#FF2E2E2E")
AboutBox.LicenseRichTextBox.ForeColor = SystemColors.Control
AboutBox.Panel1.BackColor = ColorTranslator.FromHtml("#FF121212")
' Foreground colours
AboutBox.ForeColor = SystemColors.Control
AboutBox.LicenseRichTextBox.ForeColor = SystemColors.Control
AboutBox.LabelProgramName.ForeColor = SystemColors.Control
AboutBox.LabelProgramDescription.ForeColor = SystemColors.Control
AboutBox.LabelVersion.ForeColor = SystemColors.Control
' If dark mode is enabled, set the 'Dark Mode' text value to 'Light mode'
FormMain.ToolStripMenuItemDarkMode.Text = "Light Mode"
Else
StartForm.BackColor = SystemColors.Control
StartForm.ToolsToolStripMenuTools.ForeColor = ColorTranslator.FromHtml("#FF121212")
StartForm.KeywordTextBox.BackColor = SystemColors.Control
StartForm.KeywordTextBox.ForeColor = ColorTranslator.FromHtml("#FF121212")
StartForm.SubredditTextBox.BackColor = SystemColors.Control
StartForm.SubredditTextBox.ForeColor = ColorTranslator.FromHtml("#FF121212")
StartForm.LimitNumericUpDown.BackColor = SystemColors.Control
StartForm.LimitNumericUpDown.ForeColor = ColorTranslator.FromHtml("#FF121212")
StartForm.LimitNumericUpDown.BackColor = SystemColors.Control
StartForm.LimitNumericUpDown.ForeColor = ColorTranslator.FromHtml("#FF121212")
StartForm.ListingComboBox.BackColor = SystemColors.Control
StartForm.ListingComboBox.ForeColor = ColorTranslator.FromHtml("#FF121212")
StartForm.TimeframeComboBox.BackColor = SystemColors.Control
StartForm.TimeframeComboBox.ForeColor = ColorTranslator.FromHtml("#FF121212")
StartForm.LabelKeyword.ForeColor = ColorTranslator.FromHtml("#FF121212")
StartForm.LabelSubreddit.ForeColor = ColorTranslator.FromHtml("#FF121212")
StartForm.LabelLimit.ForeColor = ColorTranslator.FromHtml("#FF121212")
StartForm.LabelListing.ForeColor = ColorTranslator.FromHtml("#FF121212")
StartForm.LabelTimeframe.ForeColor = ColorTranslator.FromHtml("#FF121212")
' Disable dark mode for the Main Form
' Background colours
FormMain.BackColor = Color.Gainsboro
FormMain.TextBoxKeyword.BackColor = SystemColors.Control
FormMain.TextBoxSubreddit.BackColor = SystemColors.Control
FormMain.NumericUpDownLimit.BackColor = SystemColors.Control
FormMain.NumericUpDownLimit.BackColor = SystemColors.Control
FormMain.ComboBoxTimeframe.BackColor = SystemColors.Control
FormMain.ComboBoxListing.BackColor = SystemColors.Control
' Foreground colours
FormMain.TextBoxKeyword.ForeColor = ColorTranslator.FromHtml("#FF121212")
FormMain.TextBoxSubreddit.ForeColor = ColorTranslator.FromHtml("#FF121212")
FormMain.NumericUpDownLimit.ForeColor = ColorTranslator.FromHtml("#FF121212")
FormMain.NumericUpDownLimit.ForeColor = ColorTranslator.FromHtml("#FF121212")
FormMain.ComboBoxListing.ForeColor = ColorTranslator.FromHtml("#FF121212")
FormMain.ComboBoxTimeframe.ForeColor = ColorTranslator.FromHtml("#FF121212")
FormMain.LabelKeyword.ForeColor = ColorTranslator.FromHtml("#FF121212")
FormMain.LabelSubreddit.ForeColor = ColorTranslator.FromHtml("#FF121212")
FormMain.LabelLimit.ForeColor = ColorTranslator.FromHtml("#FF121212")
FormMain.LabelListing.ForeColor = ColorTranslator.FromHtml("#FF121212")
FormMain.LabelTimeframe.ForeColor = ColorTranslator.FromHtml("#FF121212")
' Disable dark mode on 'Right Click Menu' items
' Background colours
FormMain.ToolStripMenuItemDarkMode.BackColor = Color.Gainsboro
FormMain.ToolStripMenuItemSavePosts.BackColor = Color.Gainsboro
FormMain.ToolStripMenuItemtoJSON.BackColor = Color.Gainsboro
FormMain.ToolStripMenuItemtoCSV.BackColor = Color.Gainsboro
FormMain.ToolStripMenuItemAbout.BackColor = Color.Gainsboro
FormMain.ToolStripMenuItemDeveloper.BackColor = Color.Gainsboro
FormMain.ToolStripMenuItemCheckUpdates.BackColor = Color.Gainsboro
FormMain.ToolStripMenuItemQuit.BackColor = Color.Gainsboro
' Foreground colours
FormMain.ToolStripMenuItemDarkMode.ForeColor = Color.Black
FormMain.ToolStripMenuItemSavePosts.ForeColor = Color.Black
FormMain.ToolStripMenuItemtoJSON.ForeColor = Color.Black
FormMain.ToolStripMenuItemtoCSV.ForeColor = Color.Black
FormMain.ToolStripMenuItemAbout.ForeColor = Color.Black
FormMain.ToolStripMenuItemDeveloper.ForeColor = Color.Black
FormMain.ToolStripMenuItemCheckUpdates.ForeColor = Color.Black
FormMain.ToolStripMenuItemQuit.ForeColor = Color.Black
' Disable dark mode for the About box
' Background colours
AboutBox.BackColor = Color.Gainsboro
AboutBox.ForeColor = SystemColors.WindowText
AboutBox.LicenseRichTextBox.BackColor = SystemColors.Control
AboutBox.LicenseRichTextBox.ForeColor = SystemColors.WindowText
AboutBox.Panel1.BackColor = Color.Gainsboro
' Foreground colours
AboutBox.Panel1.ForeColor = SystemColors.WindowText
AboutBox.LabelProgramName.ForeColor = SystemColors.WindowText
AboutBox.LabelProgramDescription.ForeColor = SystemColors.WindowText
AboutBox.LabelVersion.ForeColor = SystemColors.WindowText
' If dark mode is disabled, set the 'Light Mode' text value to 'Dark Mode'
FormMain.ToolStripMenuItemDarkMode.Text = "Dark Mode"
End If
End Sub
''' <summary>
''' Retrieves the Dark Mode setting value from 'settings.json'.
''' If the settings file doesn't exist, defaults to returning 'False' (Dark Mode off).

View File

@@ -1,324 +0,0 @@
<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()>
Partial Class StartForm
Inherits System.Windows.Forms.Form
'Form overrides dispose to clean up the component list.
<System.Diagnostics.DebuggerNonUserCode()>
Protected Overrides Sub Dispose(ByVal disposing As Boolean)
Try
If disposing AndAlso components IsNot Nothing Then
components.Dispose()
End If
Finally
MyBase.Dispose(disposing)
End Try
End Sub
'Required by the Windows Form Designer
Private components As System.ComponentModel.IContainer
'NOTE: The following procedure is required by the Windows Form Designer
'It can be modified using the Windows Form Designer.
'Do not modify it using the code editor.
<System.Diagnostics.DebuggerStepThrough()>
Private Sub InitializeComponent()
components = New ComponentModel.Container()
Dim resources As ComponentModel.ComponentResourceManager = New ComponentModel.ComponentResourceManager(GetType(StartForm))
KeywordTextBox = New TextBox()
SubredditTextBox = New TextBox()
ScrapeButton = New Button()
TimeframeComboBox = New ComboBox()
ListingComboBox = New ComboBox()
LabelKeyword = New Label()
LabelSubreddit = New Label()
LabelLimit = New Label()
LabelListing = New Label()
LabelTimeframe = New Label()
ContextMenuStrip1 = New ContextMenuStrip(components)
SaveResultsStripMenuItem = New ToolStripMenuItem()
JSONToolStripMenuItem = New ToolStripMenuItem()
CSVToolStripMenuItem = New ToolStripMenuItem()
DarkModeToolStripMenuItem = New ToolStripMenuItem()
FileMenuStrip = New MenuStrip()
ToolsToolStripMenuTools = New ToolStripMenuItem()
AboutToolStripMenuItem = New ToolStripMenuItem()
DeveloperToolStripMenuItem = 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()
SuspendLayout()
'
' KeywordTextBox
'
KeywordTextBox.BackColor = SystemColors.Window
KeywordTextBox.ForeColor = SystemColors.WindowText
KeywordTextBox.Location = New Point(89, 60)
KeywordTextBox.Name = "KeywordTextBox"
KeywordTextBox.PlaceholderText = "Keyword"
KeywordTextBox.Size = New Size(100, 23)
KeywordTextBox.TabIndex = 0
'
' SubredditTextBox
'
SubredditTextBox.Location = New Point(89, 92)
SubredditTextBox.Name = "SubredditTextBox"
SubredditTextBox.PlaceholderText = "Subreddit"
SubredditTextBox.Size = New Size(100, 23)
SubredditTextBox.TabIndex = 4
'
' ScrapeButton
'
ScrapeButton.Location = New Point(257, 191)
ScrapeButton.Name = "ScrapeButton"
ScrapeButton.Size = New Size(76, 28)
ScrapeButton.TabIndex = 6
ScrapeButton.Text = "Scrape"
ScrapeButton.UseVisualStyleBackColor = True
'
' TimeframeComboBox
'
TimeframeComboBox.FormattingEnabled = True
TimeframeComboBox.Items.AddRange(New Object() {"Hour", "Day", "Week", "Month", "Year"})
TimeframeComboBox.Location = New Point(89, 191)
TimeframeComboBox.Name = "TimeframeComboBox"
TimeframeComboBox.Size = New Size(100, 23)
TimeframeComboBox.TabIndex = 8
TimeframeComboBox.Text = "All"
'
' ListingComboBox
'
ListingComboBox.FormattingEnabled = True
ListingComboBox.Items.AddRange(New Object() {"Controversial", "Hot", "Best", "New", "Rising"})
ListingComboBox.Location = New Point(89, 157)
ListingComboBox.Name = "ListingComboBox"
ListingComboBox.Size = New Size(100, 23)
ListingComboBox.TabIndex = 9
ListingComboBox.Text = "Top"
'
' LabelKeyword
'
LabelKeyword.AutoEllipsis = True
LabelKeyword.Font = New Font("Segoe UI Semibold", 9F, FontStyle.Underline, GraphicsUnit.Point)
LabelKeyword.ForeColor = Color.Black
LabelKeyword.Location = New Point(12, 60)
LabelKeyword.Name = "LabelKeyword"
LabelKeyword.Size = New Size(56, 23)
LabelKeyword.TabIndex = 10
LabelKeyword.Text = "Keyword"
'
' LabelSubreddit
'
LabelSubreddit.AutoEllipsis = True
LabelSubreddit.Font = New Font("Segoe UI Semibold", 9F, FontStyle.Underline, GraphicsUnit.Point)
LabelSubreddit.ForeColor = Color.Black
LabelSubreddit.Location = New Point(12, 92)
LabelSubreddit.Name = "LabelSubreddit"
LabelSubreddit.Size = New Size(63, 23)
LabelSubreddit.TabIndex = 11
LabelSubreddit.Text = "Subreddit"
'
' LabelLimit
'
LabelLimit.AutoEllipsis = True
LabelLimit.Font = New Font("Segoe UI Semibold", 9F, FontStyle.Bold Or FontStyle.Underline, GraphicsUnit.Point)
LabelLimit.ForeColor = Color.Black
LabelLimit.Location = New Point(12, 125)
LabelLimit.Name = "LabelLimit"
LabelLimit.Size = New Size(56, 23)
LabelLimit.TabIndex = 12
LabelLimit.Text = "Limit"
'
' LabelListing
'
LabelListing.AutoEllipsis = True
LabelListing.Font = New Font("Segoe UI Semibold", 9F, FontStyle.Bold Or FontStyle.Underline, GraphicsUnit.Point)
LabelListing.ForeColor = Color.Black
LabelListing.Location = New Point(12, 157)
LabelListing.Name = "LabelListing"
LabelListing.Size = New Size(56, 23)
LabelListing.TabIndex = 13
LabelListing.Text = "Listing"
'
' LabelTimeframe
'
LabelTimeframe.AutoEllipsis = True
LabelTimeframe.Font = New Font("Segoe UI Semibold", 9F, FontStyle.Bold Or FontStyle.Underline, GraphicsUnit.Point)
LabelTimeframe.ForeColor = Color.Black
LabelTimeframe.Location = New Point(12, 191)
LabelTimeframe.Name = "LabelTimeframe"
LabelTimeframe.Size = New Size(71, 23)
LabelTimeframe.TabIndex = 14
LabelTimeframe.Text = "Timeframe"
'
' ContextMenuStrip1
'
ContextMenuStrip1.Items.AddRange(New ToolStripItem() {SaveResultsStripMenuItem, DarkModeToolStripMenuItem})
ContextMenuStrip1.Name = "ContextMenuStrip1"
ContextMenuStrip1.Size = New Size(144, 48)
'
' 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"
'
' JSONToolStripMenuItem
'
JSONToolStripMenuItem.AutoToolTip = True
JSONToolStripMenuItem.CheckOnClick = True
JSONToolStripMenuItem.Image = CType(resources.GetObject("JSONToolStripMenuItem.Image"), Image)
JSONToolStripMenuItem.Name = "JSONToolStripMenuItem"
JSONToolStripMenuItem.Size = New Size(185, 22)
JSONToolStripMenuItem.Text = "JSON"
'
' CSVToolStripMenuItem
'
CSVToolStripMenuItem.AutoToolTip = True
CSVToolStripMenuItem.Enabled = False
CSVToolStripMenuItem.Image = CType(resources.GetObject("CSVToolStripMenuItem.Image"), Image)
CSVToolStripMenuItem.Name = "CSVToolStripMenuItem"
CSVToolStripMenuItem.Size = New Size(185, 22)
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
FileMenuStrip.Items.AddRange(New ToolStripItem() {ToolsToolStripMenuTools})
FileMenuStrip.Location = New Point(0, 0)
FileMenuStrip.Name = "FileMenuStrip"
FileMenuStrip.Size = New Size(355, 24)
FileMenuStrip.TabIndex = 0
FileMenuStrip.Text = "MenuStrip1"
'
' ToolsToolStripMenuTools
'
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(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"
'
' DeveloperToolStripMenuItem
'
DeveloperToolStripMenuItem.AutoToolTip = True
DeveloperToolStripMenuItem.Image = CType(resources.GetObject("DeveloperToolStripMenuItem.Image"), Image)
DeveloperToolStripMenuItem.Name = "DeveloperToolStripMenuItem"
DeveloperToolStripMenuItem.Size = New Size(152, 22)
DeveloperToolStripMenuItem.Text = "Developer"
'
' CheckUpdatesToolStripMenuItem
'
CheckUpdatesToolStripMenuItem.AutoToolTip = True
CheckUpdatesToolStripMenuItem.Image = CType(resources.GetObject("CheckUpdatesToolStripMenuItem.Image"), Image)
CheckUpdatesToolStripMenuItem.Name = "CheckUpdatesToolStripMenuItem"
CheckUpdatesToolStripMenuItem.Size = New Size(152, 22)
CheckUpdatesToolStripMenuItem.Text = "Check updates"
'
' ToolStripSeparator2
'
ToolStripSeparator2.Name = "ToolStripSeparator2"
ToolStripSeparator2.Size = New Size(149, 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"
'
' LimitNumericUpDown
'
LimitNumericUpDown.Location = New Point(89, 125)
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() {10, 0, 0, 0})
'
' StartForm
'
AutoScaleDimensions = New SizeF(7F, 15F)
AutoScaleMode = AutoScaleMode.Font
BackColor = SystemColors.Control
ClientSize = New Size(355, 255)
ContextMenuStrip = ContextMenuStrip1
Controls.Add(LimitNumericUpDown)
Controls.Add(FileMenuStrip)
Controls.Add(LabelTimeframe)
Controls.Add(LabelListing)
Controls.Add(LabelLimit)
Controls.Add(LabelSubreddit)
Controls.Add(LabelKeyword)
Controls.Add(ListingComboBox)
Controls.Add(TimeframeComboBox)
Controls.Add(SubredditTextBox)
Controls.Add(ScrapeButton)
Controls.Add(KeywordTextBox)
FormBorderStyle = FormBorderStyle.FixedSingle
Icon = CType(resources.GetObject("$this.Icon"), Icon)
MainMenuStrip = FileMenuStrip
MaximizeBox = False
Name = "StartForm"
StartPosition = FormStartPosition.CenterScreen
Text = "ProgramName ProgramVersion"
ContextMenuStrip1.ResumeLayout(False)
FileMenuStrip.ResumeLayout(False)
FileMenuStrip.PerformLayout()
CType(LimitNumericUpDown, ComponentModel.ISupportInitialize).EndInit()
ResumeLayout(False)
PerformLayout()
End Sub
Friend WithEvents KeywordTextBox As TextBox
Friend WithEvents SubredditTextBox As TextBox
Friend WithEvents ScrapeButton As Button
Friend WithEvents TimeframeComboBox As ComboBox
Friend WithEvents ListingComboBox As ComboBox
Friend WithEvents LabelKeyword As Label
Friend WithEvents LabelSubreddit As Label
Friend WithEvents LabelLimit As Label
Friend WithEvents LabelListing As Label
Friend WithEvents LabelTimeframe As Label
Friend WithEvents ContextMenuStrip1 As ContextMenuStrip
Friend WithEvents FileMenuStrip As MenuStrip
Friend WithEvents ToolsToolStripMenuTools As ToolStripMenuItem
Friend WithEvents AboutToolStripMenuItem As ToolStripMenuItem
Friend WithEvents DeveloperToolStripMenuItem As ToolStripMenuItem
Friend WithEvents ToolStripSeparator2 As ToolStripSeparator
Friend WithEvents QuitToolStripMenuItem 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

View File

@@ -1,105 +0,0 @@
Imports System.IO
Imports Newtonsoft.Json
Imports Newtonsoft.Json.Linq
Public Class StartForm
ReadOnly settings As New SettingsManager()
ReadOnly ApiHandler As New ApiHandler()
''' <summary>
''' Handles the click event of the ScrapeButton.
''' Collects inputs, fetches Reddit posts based on the inputs,
''' and updates the DataGridView with the fetched posts.
''' </summary>
''' <param name="sender">The sender of the event.</param>
''' <param name="e">The EventArgs instance containing the event data.</param>
Private Sub ScrapeButton_Click(sender As Object, e As EventArgs) Handles ScrapeButton.Click
Utilities.ProcessRedditPosts(JSONToolStripMenuItem)
End Sub
''' <summary>
''' Event handler for the form load event.
''' It loads settings, toggles dark mode if necessary, checks for directories, logs first time launch, and sets the form title.
''' </summary>
''' <param name="sender">The source of the event.</param>
''' <param name="e">An EventArgs that contains the event data.</param>
Private Sub StartForm_Load(sender As Object, e As EventArgs) Handles MyBase.Load
settings.LoadSettings()
settings.ToggleDarkMode(settings.DarkMode)
Utilities.PathFinder()
Utilities.LogFirstTimeLaunch()
Me.Text = My.Application.Info.AssemblyName
End Sub
''' <summary>
''' Event handler for the 'About' menu item click.
''' It shows the 'About' box.
''' </summary>
''' <param name="sender">The source of the event.</param>
''' <param name="e">An EventArgs that contains the event data.</param>
Private Sub AboutToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles AboutToolStripMenuItem.Click
AboutBox.Show()
End Sub
''' <summary>
''' Event handler for the 'Quit' menu item click.
''' It asks the user for confirmation and closes the program if the user agrees.
''' </summary>
''' <param name="sender">The source of the event.</param>
''' <param name="e">An EventArgs that contains the event data.</param>
Private Sub QuitToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles QuitToolStripMenuItem.Click
Dim result As DialogResult = MessageBox.Show("This will close the program, continue?", "Quit", MessageBoxButtons.YesNo, MessageBoxIcon.Question)
If result = DialogResult.Yes Then
Me.Close()
End If
End Sub
''' <summary>
''' Event handler for the 'Developer' menu item click.
''' It shows the 'Developer' dialog box.
''' </summary>
''' <param name="sender">The source of the event.</param>
''' <param name="e">An EventArgs that contains the event data.</param>
Private Sub DeveloperToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles DeveloperToolStripMenuItem.Click
DeveloperForm.ShowDialog()
End Sub
''' <summary>
''' Event handler for the 'Check Updates' menu item click.
''' It checks for application updates and provides update information if a newer version is available.
''' </summary>
''' <param name="sender">The source of the event.</param>
''' <param name="e">An EventArgs that contains the event data.</param>
Private Sub CheckUpdatesToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles CheckUpdatesToolStripMenuItem.Click
Dim data As JObject = ApiHandler.CheckUpdates()
If data("tag_name").ToString = $"{My.Application.Info.Version}" Then
MessageBox.Show($"You're running the current version v{My.Application.Info.Version} of {My.Application.Info.ProductName}. Check again soon! :)", "Information", MessageBoxButtons.OK, MessageBoxIcon.Information)
Else
Dim confirm As DialogResult = MessageBox.Show($"A new version v{data("tag_name")} of {My.Application.Info.ProductName} is available, would you like to get it?
What's new in v{data("tag_name")}?
{data("body")}
", "Update", MessageBoxButtons.YesNo, MessageBoxIcon.Question)
If confirm = DialogResult.Yes Then
Shell($"cmd /c start https://github.com/bellingcat/reddit-post-scraping-tool/releases/tag/{data("tag_name")}")
End If
End If
End Sub
''' <summary>
''' Event handler for the 'Dark Mode' checkbox change event.
''' It toggles the dark mode of the application based on the checkbox status.
''' </summary>
''' <param name="sender">The source of the event.</param>
''' <param name="e">An EventArgs that contains the event data.</param>
Private Sub DarkModeToolStripMenuItem_CheckedChanged(sender As Object, e As EventArgs) Handles DarkModeToolStripMenuItem.CheckedChanged
settings.ToggleDarkMode(DarkModeToolStripMenuItem.Checked)
End Sub
End Class

View File

@@ -17,7 +17,7 @@ Public Class Utilities
If inputs.HasValue Then
' Initialize the DataGridView
DataGridViewHandler.AddColumn(PostsForm.DataGridViewPosts)
DataGridViewHandler.AddColumn(FormPosts.DataGridViewPosts)
' Fetch Reddit posts based on the inputs
Dim processor As New PostsProcessor()
@@ -29,17 +29,17 @@ Public Class Utilities
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
If PostsProcessor.PostContainsKeyword(post, inputs.Value.Keyword.ToLower(Globalization.CultureInfo.InvariantCulture)) Then
' Add the post to the DataGridView
DataGridViewHandler.AddRow(PostsForm.DataGridViewPosts, post, totalPosts)
PostsForm.Show()
DataGridViewHandler.AddRow(FormPosts.DataGridViewPosts, post, totalPosts)
FormPosts.Show()
keywordFound = True
End If
Next
' Check if the keyword was found in any posts
If Not keywordFound Then
MessageBox.Show($"Keyword `{inputs.Value.Keyword}` was not found in any of the " + posts("data")("children").Count.ToString(System.Globalization.CultureInfo.InvariantCulture) _
MessageBox.Show($"Keyword `{inputs.Value.Keyword}` was not found in any of the " + posts("data")("children").Count.ToString(Globalization.CultureInfo.InvariantCulture) _
+ $" {inputs.Value.Listing} posts from r/{inputs.Value.Subreddit}", "Not Found", MessageBoxButtons.OK, MessageBoxIcon.Warning)
End If
@@ -48,7 +48,6 @@ Public Class Utilities
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
@@ -76,8 +75,8 @@ Public Class Utilities
''' </summary>
''' <returns>
''' Tuple containing:
''' Keyword (String) - Keyword entered by user in the StartForm.
''' Subreddit (String) - Subreddit entered by user in the StartForm.
''' Keyword (String) - Keyword entered by user in theFormMain.
''' Subreddit (String) - Subreddit entered by user in theFormMain.
''' Listing (String) - Listing chosen by user in the StartForm, defaults to 'top' if none is selected.
''' Limit (Integer) - Limit entered by user in the StartForm, defaults to 10 if the entered value is over 100.
''' Timeframe (String) - Timeframe chosen by user in the StartForm, defaults to 'all' if none is selected.
@@ -86,27 +85,24 @@ Public Class Utilities
''' If keyword or subreddit are empty, Displays a warning and returns nothing.
''' </remarks>
Public Shared Function CollectInputs() As (Keyword As String, Subreddit As String, Listing As String, Limit As Integer, Timeframe As String)?
Dim keyword As String = StartForm.KeywordTextBox.Text.Trim()
Dim subreddit As String = StartForm.SubredditTextBox.Text.Trim()
' Convert the Keyword and Subreddit to lowercase using InvariantCulture
Dim listing As String = If(String.IsNullOrEmpty(StartForm.ListingComboBox.Text), "top", StartForm.ListingComboBox.Text.ToLower(System.Globalization.CultureInfo.InvariantCulture).Trim())
Dim limit As Integer = StartForm.LimitNumericUpDown.Value
Dim timeframe As String = If(String.IsNullOrEmpty(StartForm.TimeframeComboBox.Text), "all", StartForm.TimeframeComboBox.Text.ToLower(System.Globalization.CultureInfo.InvariantCulture).Trim())
Dim keyword As String = FormMain.TextBoxKeyword.Text.Trim()
Dim subreddit As String = FormMain.TextBoxSubreddit.Text.Trim()
' Convert the Listing and Subreddit to lowercase using InvariantCulture
Dim listing As String = If(String.IsNullOrEmpty(FormMain.ComboBoxListing.Text), "top", FormMain.ComboBoxListing.Text.ToLower(Globalization.CultureInfo.InvariantCulture).Trim())
Dim timeframe As String = If(String.IsNullOrEmpty(FormMain.ComboBoxTimeframe.Text), "all", FormMain.ComboBoxTimeframe.Text.ToLower(Globalization.CultureInfo.InvariantCulture).Trim())
Dim limit As Integer = FormMain.NumericUpDownLimit.Value
' Validate inputs
If String.IsNullOrEmpty(keyword) Then
MessageBox.Show("Keyword should not be empty", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning)
If String.IsNullOrEmpty(keyword) AndAlso String.IsNullOrEmpty(subreddit) Then
MessageBox.Show("Keyword and Subreddit fields should not be empty.", "Invalid Inputs", MessageBoxButtons.OK, MessageBoxIcon.Warning)
Return Nothing
ElseIf String.IsNullOrEmpty(keyword) Then
MessageBox.Show("Keyword field should not be empty.", "Invalid Input", MessageBoxButtons.OK, MessageBoxIcon.Warning)
Return Nothing
ElseIf String.IsNullOrEmpty(subreddit) Then
MessageBox.Show("Subreddit field should not be empty.", "Invalid Input", 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
@@ -174,7 +170,6 @@ First launched on: {DateTime.Now}"
LicenseNotice()
File.WriteAllText(filePath, textToWrite)
Else
' DO NOTHING
End If
End Sub
End Class

View File

@@ -7,18 +7,18 @@ packages = ["rpst"]
[project]
name = "reddit-post-scraping-tool"
version = "1.4.1.0"
version = "1.7.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"}
keywords = ["osint", "reddit-crawler", "reddit-scraping", "reddit"]
keywords = ["reddit-crawler", "reddit-scraping", "reddit", "reddit-api"]
authors = [{name = "Richard Mwewa", email = "rly0nheart@duck.com"}]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Programming Language :: Python :: 3",
"Programming Language :: Visual Basic",
"Intended Audience :: Information Technology",
"Intended Audience :: End Users/Desktop",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Natural Language :: English"
@@ -26,6 +26,7 @@ classifiers = [
dependencies = [
"rich",
"glyphoji",
"requests",
]
@@ -35,4 +36,4 @@ documentation = "https://github.com/bellingcat/reddit-post-scraping-tool/wiki"
repository = "https://github.com/bellingcat/reddit-post-scraping-tool.git"
[project.scripts]
rpst = "rpst.__main:run"
rpst = "rpst.main:run"

1
rpst/__init__.py Normal file
View File

@@ -0,0 +1 @@

View File

@@ -1,201 +0,0 @@
import json
import logging
import argparse
import requests
from rich.tree import Tree
from rich import print as xprint
from rich.markdown import Markdown
from rich.logging import RichHandler
def write_post_data(post_data: dict, filename: str):
"""
Writes post data to a specified JSON file.
:param post_data: A dictionary containing post data.
:param filename: The name of the file to which post data will be written.
"""
# Write the data to a JSON file
with open(filename + ".json", 'a') as file:
file.write(json.dumps(post_data))
file.write('\n') # write a newline to separate posts
log.info(f"Post data written to '{file.name}'")
def check_updates(version_tag: str):
"""
This function checks if there's a new release of a project on GitHub. If there is, it logs an
information message and prints the release notes.
:param version_tag: A string representing the current version of the project.
"""
# Make a GET request to the GitHub API to get the latest release of the project
response = requests.get("https://api.github.com/repos/bellingcat/reddit-post-scraping-tool/releases/latest").json()
# Check if the latest release's tag matches the current version tag
if response['tag_name'] != version_tag:
# If not, convert the release notes from Markdown to HTML
raw_release_notes = response['body']
markdown_release_notes = Markdown(raw_release_notes)
# Log an info message about the new release
log.info(
f"A new release of RPST is available ({response['tag_name']}). "
f"Run 'pip install --upgrade reddit-post-scraping-tool' to get the updates."
)
# Print the release notes
xprint(markdown_release_notes)
def format_post_data(post: dict, keyword: str, output: bool):
"""
This function extracts relevant data from a Reddit post and displays it in a tree structure,
followed by the post's selftext.
:param post: A dictionary containing the data of a Reddit post.
:param keyword: The keyword that is used to find posts, in his case gets uses as the filename.
:param output: If specified, all found posts will be written to a json file.
"""
# Define the data to extract from the post
post_data = {
'Author': post['data']['author'],
'ID': post['data']['id'],
'Subreddit': post["data"]["subreddit_name_prefixed"],
'Visibility': post['data']['subreddit_type'],
'Thumbnail': post["data"]["thumbnail"],
'NSFW': post['data']['over_18'],
'Gilded': post['data']['gilded'],
'Upvotes': post["data"]["ups"],
'Upvote ratio': post["data"]["upvote_ratio"],
'Downvotes': post["data"]["downs"],
'Awards': post["data"]["total_awards_received"],
'Top award': post['data']['top_awarded_type'],
'Is crosspostable?': post['data']['is_crosspostable'],
'Score': post["data"]["score"],
'Category': post['data']['category'],
'Domain': post["data"]["domain"],
'Created': post['data']['created'],
'Approved at': post['data']['approved_at_utc'],
'Approved by': post['data']['approved_by'],
}
if output:
write_post_data(filename=keyword, post_data=post_data)
# Create a tree structure with the post's title as the root
post_tree = Tree("\n" + post['data']['title'])
# Add each piece of extracted data as a branch of the tree
for post_key, post_value in post_data.items():
post_tree.add(f"{post_key}: {post_value}")
# Print the tree structure
xprint(post_tree)
# Print the post's selftext
print(post['data']['selftext'] + "\n")
def get_posts(arguments: argparse):
"""
Scrapes a given subreddit for posts that contain a specified keyword.
The search is limited by the number of posts and timeframe specified. The results are either
printed to the console or saved to a specified file, based on the 'output' argument.
:param arguments: Namespace object from argparse.
Expected Object Attributes
--------------------------
- keyword: The keyword to search for in the posts.
- subreddit: The subreddit to scrape.
- listing: The type of posts to scrape. This could be 'hot', 'new', etc.
- timeframe: The timeframe from which to scrape posts. This could be 'day', 'week', etc.
- limit: The maximum number of posts to scrape.
- json: If specified, all found posts will be written to a json file.
Also logs the number of posts in which the keyword was found.
"""
keyword = arguments.keyword
subreddit = arguments.subreddit
listing = arguments.listing
timeframe = arguments.timeframe
limit = arguments.limit
json_output = arguments.json
# Start a new session
session = requests.session()
# Set the User-Agent to mimic a Safari browser on a Mac
session.headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, '
'like Gecko) Version/14.1.1 Safari/605.1.15'}
# Send a GET request to the specified subreddit and listing,
# limiting the response by the specified limit and timeframe
response = session.get(f'https://reddit.com/r/{subreddit}/{listing}'
f'.json?limit={limit}&t={timeframe}').json()
# Initialize a counter for the number of posts found that contain the keyword
found_posts = 0
# Loop through each post in the response
for post in response['data']['children']:
# If the keyword is found in the post's selftext or title, increment the counter and process the post
if keyword.lower() in post['data']['selftext'] or keyword.lower() in post['data']['title']:
found_posts += 1
format_post_data(post=post, keyword=keyword, output=json_output)
# Log the number of posts in which the keyword was found
log.info(f"Keyword ('{keyword}') was found in {found_posts}/{len(response['data']['children'])} "
f"{listing} posts from r/{subreddit}.")
def create_parser():
"""
Creates and configures an argument parser for the command line arguments.
:return: A configured argparse.ArgumentParser object ready to parse the command line arguments.
"""
parser = argparse.ArgumentParser(
description='RPST: Reddit Post Scraping Tool —by Richard Mwewa | https://about.me/rly0nheart',
epilog='Given a subreddit name and a keyword, '
'RPST returns all top (by default) posts that contain the specified keyword.'
)
parser.add_argument('-k', '--keyword', help='The keyword to search for in the posts.', required=True)
parser.add_argument('-s', '--subreddit', help='The subreddit to scrape.', required=True)
parser.add_argument(
'-c', '--limit',
help='The maximum number of posts to scrape (1-100). (default: %(default)s)',
default=10,
type=int,
choices=range(1, 101) # This enforces that the limit must be between 1 and 100 inclusive.
)
parser.add_argument(
'-l', '--listing',
default='top',
const='top',
nargs='?',
choices=['controversial', 'hot', 'best', 'new', 'rising'],
help='The type of posts to scrape (default: %(default)s)'
)
parser.add_argument(
'-t', '--timeframe',
default='all',
const='all',
nargs='?',
choices=['hour', 'day', 'week', 'month', 'year', 'all'],
help='The timeframe from which to scrape posts (default: %(default)s)'
)
parser.add_argument(
'-j', '--json',
help='Write all found posts to a json file.',
action='store_true'
)
return parser
logging.basicConfig(level="NOTSET", format="%(message)s",
handlers=[RichHandler(markup=True, log_time_format='[%H:%M:%S%p]')])
log = logging.getLogger("rich")

View File

@@ -1,5 +1,7 @@
from datetime import datetime
from rpst.__rpst import log, get_posts, check_updates, create_parser
from .rpst import get_posts
from .utils import create_parser, set_loglevel, check_updates
def run():
@@ -10,20 +12,22 @@ def run():
# Create a parser and parse the command line arguments
parser = create_parser()
arguments = parser.parse_args()
args = parser.parse_args()
log = set_loglevel(args=args)
# Record the start time
start_time = datetime.now()
try:
# Check for updates
check_updates(version_tag="1.4.1.0")
check_updates(version_tag="1.7.0.0")
# Get posts with the provided/parsed arguments
get_posts(arguments=arguments)
get_posts(args=args)
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.')
log.info(f"Finished in {datetime.now() - start_time} seconds.")

131
rpst/rpst.py Normal file
View File

@@ -0,0 +1,131 @@
import argparse
from datetime import datetime
import requests
from glyphoji import glyph
from rich.tree import Tree
from rich import print as xprint
from .utils import convert_timestamp_to_datetime, write_post_data
def create_post_branch(post: dict, keyword: str, tree: Tree, args: argparse) -> Tree:
"""
This function extracts relevant data from a Reddit post and adds it in a tree branch structure,
followed by the post's selftext.
:param post: A dictionary containing the data of a Reddit post.
:param keyword: The keyword that is used to find posts, in his case gets uses as the filename.
:param tree: Tree where the post branch will be added.
:param args: A namespace object from argparse.
:returns: The main tree with added post branches.
"""
# Define the data to extract from the post.
post_data = {
# "Author": post["data"]["author"],
f"{glyph.id_button} ID": post["data"]["id"],
f"{glyph.people_hugging} Subreddit": post["data"]["subreddit_name_prefixed"],
f"{glyph.face_with_peeking_eye} Visibility": post["data"]["subreddit_type"],
f"{glyph.framed_picture} Thumbnail": post["data"]["thumbnail"],
f"{glyph.white_question_mark} Gilded": post["data"]["gilded"],
f"{glyph.up_arrow} Upvotes": post["data"]["ups"],
f"{glyph.chart_increasing} Upvote ratio": post["data"]["upvote_ratio"],
f"{glyph.down_arrow} Downvotes": post["data"]["downs"],
f"{glyph.trophy} Awards": post["data"]["total_awards_received"],
f"{glyph.trophy} Top award": post["data"]["top_awarded_type"],
f"{glyph.no_one_under_eighteen} Is NSFW?": post["data"]["over_18"],
f"{glyph.left_arrow_curving_right} Is crosspostable?": post["data"][
"is_crosspostable"
],
f"{glyph.bar_chart} Score": post["data"]["score"],
f"{glyph.card_file_box} Category": post["data"]["category"],
f"{glyph.globe_with_meridians} Domain": post["data"]["domain"],
f"{glyph.calendar} Posted on": convert_timestamp_to_datetime(
post["data"]["created"]
),
f"{glyph.calendar} Approved at": post["data"]["approved_at_utc"],
f"{glyph.bust_in_silhouette} Approved by": post["data"]["approved_by"],
}
# Add the post's branch to the main tree.
post_branch = tree.add(f"{glyph.scroll} {post['data']['title']}")
# Add each piece of extracted data as a branch of the post_branch.
for post_key, post_value in post_data.items():
post_branch.add(f"{post_key}: {post_value}", style="dim")
# This ensures that the post's selftext is also added to the written json/csv file.
post_data[f"{glyph.clipboard} Text"] = post["data"]["selftext"]
write_post_data(
filename=keyword, post_data=post_data, tree_branch=post_branch, args=args
)
post_branch.add(post["data"]["selftext"], style="italic")
return tree
def get_posts(args: argparse):
"""
Scrapes a given subreddit for posts that contain a specified keyword.
The search is limited by the number of posts and timeframe specified.
:param args: Namespace object from argparse.
Expected Object Attributes
--------------------------
- keyword: The keyword to search for in the posts.
- subreddit: The subreddit to scrape.
- listing: The type of posts to scrape. This could be 'hot', 'new', etc.
- timeframe: The timeframe from which to scrape posts. This could be 'day', 'week', etc.
- limit: The maximum number of posts to scrape.
- json: If specified, all found posts will be written to a json file.
"""
keyword = args.keyword
subreddit = args.subreddit
listing = args.listing
timeframe = args.timeframe
limit = args.limit
# Create main result tree.
main_tree = Tree(
f"[bold]{glyph.calendar} {datetime.now()}[/]", guide_style="bold bright_blue"
)
# Start a new session
session = requests.session()
# Set the User-Agent to mimic a Safari browser on a Mac.
session.headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, "
"like Gecko) Version/14.1.1 Safari/605.1.15"
}
# Send a GET request to the specified subreddit and listing,
# limiting the response by the specified limit and timeframe.
response = session.get(
f"https://reddit.com/r/{subreddit}/{listing}"
f".json?limit={limit}&t={timeframe}"
).json()
# Initialize a counter for the number of posts found that contain the keyword.
found_posts = 0
# Loop through each post in the response
for post_index, post in enumerate(response["data"]["children"], start=1):
# 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"]
):
# Create a branch for found post(s) and show post index and post author as the title
found_tree = main_tree.add(
f"{glyph.bust_in_silhouette} #{post_index} by [bold]@{post['data']['author']}[/]"
)
found_posts += 1
create_post_branch(post=post, keyword=keyword, tree=found_tree, args=args)
# Log the number of posts in which the keyword was found
main_tree.add(
f"{glyph.check_mark_button} Keyword ('{keyword}') was found in "
f"{found_posts}/{len(response['data']['children'])} {listing} posts from r/{subreddit}."
)
xprint(main_tree)

202
rpst/utils.py Normal file
View File

@@ -0,0 +1,202 @@
import os
import csv
import json
import logging
import argparse
from datetime import datetime
import requests
from glyphoji import glyph
from rich.tree import Tree
from rich import print as xprint
from rich.markdown import Markdown
from rich.logging import RichHandler
def convert_timestamp_to_datetime(timestamp: int) -> str:
"""
Converts a Unix timestamp to a formatted datetime string.
:param timestamp: The Unix timestamp to be converted.
:return: A formatted datetime string in the format "dd MMMM yyyy, hh:mm:ssAM/PM".
"""
utc_from_timestamp = datetime.utcfromtimestamp(timestamp)
datetime_object = utc_from_timestamp.strftime("%d %B %Y, %I:%M:%S%p")
return datetime_object
def create_parser():
"""
Creates and configures an argument parser for the command line arguments.
:return: A configured argparse.ArgumentParser object ready to parse the command line arguments.
"""
parser = argparse.ArgumentParser(
description="RPST (Reddit Post Scraping Tool) —by Richard Mwewa | https://about.me/rly0nheart",
epilog="Given a subreddit name and a keyword, "
"RPST returns all top (by default) posts that contain the specified keyword.",
)
parser.add_argument(
"-k", "--keyword", help="The keyword to search for in the posts.", required=True
)
parser.add_argument(
"-s", "--subreddit", help="The subreddit to scrape.", required=True
)
parser.add_argument(
"-c",
"--limit",
help="The maximum number of posts to scrape (1-100). (default: %(default)s)",
default=10,
type=int,
choices=range(
1, 101
), # This enforces that the limit must be between 1 and 100 inclusive.
)
parser.add_argument(
"-l",
"--listing",
default="top",
const="top",
nargs="?",
choices=["controversial", "hot", "best", "new", "rising"],
help="The type of posts to scrape (default: %(default)s)",
)
parser.add_argument(
"-t",
"--timeframe",
default="all",
const="all",
nargs="?",
choices=["hour", "day", "week", "month", "year", "all"],
help="The timeframe from which to scrape posts (default: %(default)s)",
)
parser.add_argument(
"--json",
help="Write all found posts to a json file.",
action="store_true",
)
parser.add_argument(
"--csv",
help="Write all found posts to a csv file.",
action="store_true",
)
parser.add_argument(
"-d",
"--debug",
help="run rpst in debug mode (show network logs)",
action="store_true",
)
return parser
def check_updates(version_tag: str):
"""
This function checks if there's a new release of a project on GitHub. If there is, it logs an
information message and prints the release notes.
:param version_tag: A string representing the current version of the project.
"""
# Make a GET request to the GitHub API to get the latest release of the project.
response = requests.get(
"https://api.github.com/repos/bellingcat/reddit-post-scraping-tool/releases/latest"
).json()
# Check if the latest release's tag matches the current version tag.
if response["tag_name"] != version_tag:
# If not, convert the release notes from Markdown to HTML.
raw_release_notes = response["body"]
# Log an info message about the new release.
xprint(
f"{glyph.up_arrow} A new release of RPST is available ({response['tag_name']}). "
f"Run 'pip install --upgrade reddit-post-scraping-tool' to get the updates."
)
# Print the release notes.
xprint(Markdown(raw_release_notes))
def set_loglevel(args: argparse) -> logging.getLogger:
"""
Configures the logging level based on the provided arguments.
If `args.debug` is True, the logging level is set to "NOTSET," allowing all log messages to be displayed.
Otherwise, the logging level is set to "INFO," and only informational and higher-severity messages are displayed.
The function also configures a RichHandler for formatting the log messages,
including a specific time format and hiding the log level.
:param args: A namespace object from argparse containing the debugging option (args.debug).
:return: A logger object associated with the name "rich."
"""
if args.debug:
logging.basicConfig(
level="NOTSET",
format="%(message)s",
handlers=[
RichHandler(
markup=True, log_time_format="[%H:%M:%S%p]", show_level=False
)
],
)
else:
logging.basicConfig(
level="INFO",
format="%(message)s",
handlers=[
RichHandler(
markup=True, log_time_format="[%H:%M:%S%p]", show_level=False
)
],
)
return logging.getLogger("rich")
def write_post_data(post_data: dict, filename: str, args, tree_branch: Tree):
"""
Writes post data to a specified JSON or CSV file based on the args provided, and updates
the provided tree with the status.
:param post_data: A dictionary containing post data.
:param filename: The name of the file to which post data will be written.
:param args: A namespace object from argparse containing the output format options (args.json or args.csv).
:param tree_branch: A rich Tree object to which status information will be added.
"""
home_directory = os.path.expanduser("~")
if args.json:
json_file_path = os.path.join(home_directory, f"{filename}.json")
with open(json_file_path, "a", encoding="utf-8") as file:
file.write(json.dumps(post_data, ensure_ascii=False))
file.write("\n") # Separate posts with newline
tree_branch.add(
f"{glyph.page_facing_up} JSON data successfully written/appended to file: "
f"[italic][link file://{json_file_path}]{json_file_path}[/]"
)
else:
tree_branch.add(
f"{glyph.cross_mark_button} JSON data writing operation was skipped. No changes made."
)
if args.csv:
csv_file_path = os.path.join(home_directory, f"{filename}.csv")
with open(csv_file_path, "a", newline="", encoding="utf-8") as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=post_data.keys())
# Write headers if file is empty
if csvfile.tell() == 0:
writer.writeheader()
writer.writerow(post_data)
tree_branch.add(
f"{glyph.page_facing_up} CSV data successfully written/appended to file: "
f"[italic][link file://{csv_file_path}]{csv_file_path}[/]"
)
else:
tree_branch.add(
f"{glyph.cross_mark_button} CSV data writing operation was skipped. No changes made."
)