19 Commits

Author SHA1 Message Date
Richard Mwewa
e88e1a2d5a Update README.md 2023-08-06 05:37:08 +02:00
Richard Mwewa
71b65753cf Update __main.py
Added json file logger for found posts.
2023-08-06 05:35:46 +02:00
Richard Mwewa
002dd57c0d Update __rpst_.py
Added json logger for found posts
2023-08-06 05:34:13 +02:00
Richard Mwewa
6e9f97c444 Update README.md 2023-08-06 04:45:36 +02:00
Richard Mwewa
1ff6d2c9c0 Update pyproject.toml 2023-08-06 02:24:17 +02:00
Richard Mwewa
b356a6beaa Merge pull request #3 from bellingcat/dev
Dev
2023-08-06 02:19:33 +02:00
Richard Mwewa
618aaa45ba Update dependabot.yml 2023-08-06 02:12:39 +02:00
Richard Mwewa
f321accfbb Add files via upload
New version of RPST GUI
2023-08-06 02:12:11 +02:00
Richard Mwewa
e2e9228bec Delete Reddit Post Scraping Tool directory 2023-08-06 02:11:06 +02:00
Richard Mwewa
90e7fefa7f Update Dockerfile 2023-08-05 23:50:51 +02:00
Richard Mwewa
64ebdca6ee Update pyproject.toml 2023-08-05 23:50:09 +02:00
Richard Mwewa
ca0458f328 Update and rename main.py to __main.py
Refactored and added doc strings to code.
2023-08-05 23:48:23 +02:00
Richard Mwewa
bc10b3020e Update and rename reddit_post_scraping_tool.py to __rpst_.py
Refactored and added doc strings to code
2023-08-05 23:47:01 +02:00
Richard Mwewa
fc0c62a1ee Update README.md 2023-06-07 15:29:57 +02:00
Richard Mwewa
151183765b Update pyproject.toml
Added line `[tool.setuptools]`. Fixes error that was being returned during compilation:

`
* Getting build dependencies for sdist...

error: Multiple top-level packages discovered in a flat-layout: ["reddit_post_scraping_tool", "Reddit Post Scraping Tool"]
`
2023-04-13 11:40:38 +02:00
Richard Mwewa
210beccce8 Update pyproject.toml 2023-03-07 01:37:54 +02:00
Richard Mwewa
3a3a0b67dc Update to v1.4.0.0 2023-03-07 01:34:19 +02:00
Richard Mwewa
7399683352 Update README.md 2023-03-07 01:03:11 +02:00
Richard Mwewa
b536b8245a Delete Reddit Post Scraping Tool directory 2023-03-07 00:51:45 +02:00
41 changed files with 1497 additions and 719 deletions

View File

@@ -8,7 +8,7 @@ updates:
- package-ecosystem: "nuget"
schedule:
interval: "daily"
directory: "Reddit Post Scraping Tool"
directory: "RPST GUI"
ignore:
- dependency-name: "Newtonsoft.Json"
- package-ecosystem: "pip"

View File

@@ -6,6 +6,6 @@ WORKDIR /app
COPY . .
RUN pip install --upgrade pip && pip install build && python -m build && pip install dist/*.whl
RUN pip install --upgrade pip && pip install .
ENTRYPOINT ["reddit_post_scraping_tool"]
ENTRYPOINT ["rpst"]

View File

@@ -1,30 +1,35 @@
# Reddit Post Scraping Tool
# RPST (Reddit Post Scraping Tool)
Given a subreddit name and a keyword, this script will return all posts from a specified listing (default is 'top') that contain the provided keyword.
[![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)
![Screenshot 2023-02-10 195818](https://user-images.githubusercontent.com/74001397/218163494-245f6676-1fb3-4680-a6b5-bd15fb1dea5e.png)
![Screenshot_20230210_193329](https://user-images.githubusercontent.com/74001397/218158084-9295abb7-df33-4f86-8df8-e109cac7cde6.png)
![2023-08-06_04-30](https://github.com/bellingcat/reddit-post-scraping-tool/assets/74001397/e924ec32-8786-41ab-af59-bd5fca0cdb57)
![2023-08-06_04-32](https://github.com/bellingcat/reddit-post-scraping-tool/assets/74001397/04d6de20-2e02-4dea-a1cc-30c611802acf)
# Features (GUI)
- [x] Auto dark mode from 6pm - 6am
- [x] Saves results to a JSON
- [ ] Other features coming soon...
# TODO (GUI)
- [ ] Make it a stand alone executable
- [ ] Add manual dark mode option, that will be remembered in all sessions
# ✅ Features
## GUI
- [x] Dark mode (Right-click)
- [x] Saves results to a JSON (Right-click)
- [x] Logs errors to a file
## CLI
- [x] Saves results to a JSON (-j/--json)
- [x] Automatically checks for new updates. Notifies user if update were found.
# 📃 TODO
## GUI
- [ ] Make it installable with a setup.exe/setup.msi file.
- [x] Add manual dark mode option, that will be persistent in all sessions
- [ ] Make it save results to a CSV file
# Wiki
# 📖 Wiki
[Refer to the Wiki](https://github.com/rly0nheart/reddit-post-scraping-tool/wiki) for installation instructions, in addition to all other documentation.
# Note
> This is one of the projects I am working on, while learning Visual Basic, so the implementation/code may be messed up. If that's the case, please feel free to open a pull request using the available templates. Otherwise, enjoy!
# 😁 Donations
If you like `RPST` and would like to show support, you can Buy A Coffee for the developer using the button below
# Donations
If you like `Reddit Post Scraping Tool` and would like to show support, you can Buy A Coffee for the developer using the button below
<a href="https://www.buymeacoffee.com/189381184" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174"></a>
<a href="https://www.buymeacoffee.com/_rly0nheart" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174"></a>
Your support will be much appreciated😊

View File

@@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.4.33213.308
MinimumVisualStudioVersion = 10.0.40219.1
Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "Reddit Post Scraping Tool", "Reddit Post Scraping Tool\Reddit Post Scraping Tool.vbproj", "{46C2541E-6F65-461A-A479-F65D445C36EA}"
Project("{778DAE3C-4631-46EA-AA77-85C1314464D9}") = "RPST", "RPST\RPST.vbproj", "{46C2541E-6F65-461A-A479-F65D445C36EA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution

View File

@@ -1,5 +1,5 @@
<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()> _
Partial Class AboutForm
Partial Class AboutBox
Inherits System.Windows.Forms.Form
'Form overrides dispose to clean up the component list.
@@ -22,11 +22,11 @@ Partial Class AboutForm
'Do not modify it using the code editor.
<System.Diagnostics.DebuggerStepThrough()> _
Private Sub InitializeComponent()
Dim resources As ComponentModel.ComponentResourceManager = New ComponentModel.ComponentResourceManager(GetType(AboutForm))
Dim resources As ComponentModel.ComponentResourceManager = New ComponentModel.ComponentResourceManager(GetType(AboutBox))
PictureBox1 = New PictureBox()
LicenseRichTextBox = New RichTextBox()
Label1 = New Label()
Label2 = New Label()
LicenseLabel = New Label()
ProgramNameLabel = New Label()
DescriptionLabel = New Label()
VersionLabel = New Label()
WikiLinkLabel = New LinkLabel()
@@ -51,33 +51,37 @@ Partial Class AboutForm
LicenseRichTextBox.ReadOnly = True
LicenseRichTextBox.Size = New Size(513, 342)
LicenseRichTextBox.TabIndex = 1
LicenseRichTextBox.Text = ""'
' Label1
LicenseRichTextBox.Text = "License notice"
'
Label1.AutoSize = True
Label1.Font = New Font("Microsoft JhengHei UI", 9.75F, FontStyle.Regular, GraphicsUnit.Point)
Label1.Location = New Point(28, 150)
Label1.Name = "Label1"
Label1.Size = New Size(52, 17)
Label1.TabIndex = 2
Label1.Text = "License"'
' Label2
' LicenseLabel
'
LicenseLabel.AutoSize = True
LicenseLabel.Font = New Font("Microsoft JhengHei UI", 9.75F, FontStyle.Regular, GraphicsUnit.Point)
LicenseLabel.Location = New Point(28, 150)
LicenseLabel.Name = "LicenseLabel"
LicenseLabel.Size = New Size(52, 17)
LicenseLabel.TabIndex = 2
LicenseLabel.Text = "License"
'
' ProgramNameLabel
'
ProgramNameLabel.AutoSize = True
ProgramNameLabel.Font = New Font("Microsoft JhengHei", 14.25F, FontStyle.Bold, GraphicsUnit.Point)
ProgramNameLabel.Location = New Point(126, 47)
ProgramNameLabel.Name = "ProgramNameLabel"
ProgramNameLabel.Size = New Size(66, 24)
ProgramNameLabel.TabIndex = 3
ProgramNameLabel.Text = "Name"
'
Label2.AutoSize = True
Label2.Font = New Font("Microsoft JhengHei", 14.25F, FontStyle.Bold, GraphicsUnit.Point)
Label2.Location = New Point(126, 47)
Label2.Name = "Label2"
Label2.Size = New Size(246, 24)
Label2.TabIndex = 3
Label2.Text = "Reddit Post Scraping Tool"'
' DescriptionLabel
'
DescriptionLabel.AutoSize = True
DescriptionLabel.Font = New Font("Segoe UI", 9F, FontStyle.Regular, GraphicsUnit.Point)
DescriptionLabel.Location = New Point(126, 81)
DescriptionLabel.Name = "DescriptionLabel"
DescriptionLabel.Size = New Size(0, 15)
DescriptionLabel.Size = New Size(67, 15)
DescriptionLabel.TabIndex = 4
DescriptionLabel.Text = "Description"
'
' VersionLabel
'
@@ -85,9 +89,10 @@ Partial Class AboutForm
VersionLabel.Font = New Font("Segoe UI", 9F, FontStyle.Italic, GraphicsUnit.Point)
VersionLabel.Location = New Point(500, 54)
VersionLabel.Name = "VersionLabel"
VersionLabel.Size = New Size(42, 15)
VersionLabel.Size = New Size(46, 15)
VersionLabel.TabIndex = 5
VersionLabel.Text = "Label4"'
VersionLabel.Text = "Version"
'
' WikiLinkLabel
'
WikiLinkLabel.AutoSize = True
@@ -96,8 +101,9 @@ Partial Class AboutForm
WikiLinkLabel.Size = New Size(30, 15)
WikiLinkLabel.TabIndex = 6
WikiLinkLabel.TabStop = True
WikiLinkLabel.Text = "Wiki"'
' AboutForm
WikiLinkLabel.Text = "Wiki"
'
' AboutBox
'
AutoScaleDimensions = New SizeF(7F, 15F)
AutoScaleMode = AutoScaleMode.Font
@@ -106,18 +112,18 @@ Partial Class AboutForm
Controls.Add(WikiLinkLabel)
Controls.Add(VersionLabel)
Controls.Add(DescriptionLabel)
Controls.Add(Label2)
Controls.Add(Label1)
Controls.Add(ProgramNameLabel)
Controls.Add(LicenseLabel)
Controls.Add(LicenseRichTextBox)
Controls.Add(PictureBox1)
FormBorderStyle = FormBorderStyle.FixedSingle
Icon = CType(resources.GetObject("$this.Icon"), Icon)
MaximizeBox = False
MinimizeBox = False
Name = "AboutForm"
Name = "AboutBox"
ShowInTaskbar = False
StartPosition = FormStartPosition.CenterScreen
Text = "About - Reddit Post Scraping Tool"
Text = "About"
CType(PictureBox1, ComponentModel.ISupportInitialize).EndInit()
ResumeLayout(False)
PerformLayout()
@@ -125,8 +131,8 @@ Partial Class AboutForm
Friend WithEvents PictureBox1 As PictureBox
Friend WithEvents LicenseRichTextBox As RichTextBox
Friend WithEvents Label1 As Label
Friend WithEvents Label2 As Label
Friend WithEvents LicenseLabel As Label
Friend WithEvents ProgramNameLabel As Label
Friend WithEvents DescriptionLabel As Label
Friend WithEvents VersionLabel As Label
Friend WithEvents WikiLinkLabel As LinkLabel

View File

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

36
RPST GUI/RPST/AboutBox.vb Normal file
View File

@@ -0,0 +1,36 @@
Public Class AboutBox
Public Property LicenseText As String = "MIT License
Copyright (c) 2023-2024 Richard Mwewa
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the ""Software""), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE."
Private Sub AboutForm_Load(sender As Object, e As EventArgs) Handles MyBase.Load
ProgramNameLabel.Text = My.Application.Info.AssemblyName
DescriptionLabel.Text = "Given a subreddit name and a keyword,
RPST returns all top posts
(by default) that contain the specified keyword."
VersionLabel.Text = $"v{My.Application.Info.Version}"
LicenseRichTextBox.Text = LicenseText
End Sub
Private Sub LinkLabel1_LinkClicked(sender As Object, e As LinkLabelLinkClickedEventArgs) Handles WikiLinkLabel.LinkClicked
Shell("cmd /c start https://github.com/bellingcat/reddit-post-scraping-tool/wiki")
End Sub
End Class

View File

@@ -0,0 +1,50 @@
Imports System.IO
Imports System.Net.Http
Imports Newtonsoft.Json
Imports Newtonsoft.Json.Linq
''' <summary>
''' Handles requests to Reddit and Github APIs.
''' </summary>
Public Class ApiHandler
Public Property LogFile As String = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RedditPostScrapingTool", "logs", $"debug.log")
Public Property Headers As String = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1.2 Safari/605.1.15"
Public Property UpdatesEndpoint As String = "https://api.github.com/repos/bellingcat/reddit-post-scraping-tool/releases/latest"
''' <summary>
''' Scrape Reddit data.
''' </summary>
''' <returns>Json object containing scraped data.</returns>
Public Function ScrapeReddit(subreddit As String, listing As String, limit As Integer, timeframe As String) As JObject
Dim ApiEndpoint As String = $"https://reddit.com/r/{subreddit}/{listing}.json?limit={limit}&t={timeframe}"
Return GetJObjectFromEndpoint(ApiEndpoint)
End Function
''' <summary>
''' Gets remote version information from the repository release page.
''' </summary>
''' <returns>Json object containing update data.</returns>
Public Function CheckUpdates() As JObject
Return GetJObjectFromEndpoint(UpdatesEndpoint)
End Function
Private Function GetJObjectFromEndpoint(endpoint As String) As JObject
Try
Using httpClient As New HttpClient()
httpClient.DefaultRequestHeaders.Add("User-Agent", headers)
Dim response As HttpResponseMessage = httpClient.GetAsync(endpoint).Result
If response.IsSuccessStatusCode Then
Dim json As String = response.Content.ReadAsStringAsync().Result
Dim data As JObject = JsonConvert.DeserializeObject(Of JObject)(json)
Return data
Else
MessageBox.Show(response.ReasonPhrase, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
End If
End Using
Catch ex As Exception
My.Computer.FileSystem.WriteAllText(LogFile, $"{DateTime.Now}: {ex}{Environment.NewLine}", True)
MessageBox.Show($"{ex.Message}. Please see the debug log '{LogFile}' for more information.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
End Try
Return New JObject()
End Function
End Class

View File

@@ -0,0 +1,67 @@
Imports Newtonsoft.Json.Linq
Public Class DataGridViewHandler
''' <summary>
''' Initializes the DataGridView by clearing any existing data and setting up the necessary columns.
''' </summary>
''' <param name="dataGridView">The DataGridView to be initialized.</param>
Public Shared Sub AddColumn(dataGridView As DataGridView)
' Clear the Columns and Rows before adding Items to them
PostsForm.DataGridViewPosts.Rows.Clear()
PostsForm.DataGridViewPosts.Columns.Clear()
PostsForm.DataGridViewPosts.Columns.Add("PostCount", "Post Number")
PostsForm.DataGridViewPosts.Columns.Add("PostAuthor", "Author")
PostsForm.DataGridViewPosts.Columns.Add("PostID", "ID")
PostsForm.DataGridViewPosts.Columns.Add("PostSubreddit", "Subreddit")
PostsForm.DataGridViewPosts.Columns.Add("SubredditVisibility", "Subreddit Visibility")
PostsForm.DataGridViewPosts.Columns.Add("PostThumbnail", "Thumbnail")
PostsForm.DataGridViewPosts.Columns.Add("PostIsNSFW", "NSFW")
PostsForm.DataGridViewPosts.Columns.Add("PostIsGilded", "Gilded")
PostsForm.DataGridViewPosts.Columns.Add("PostUpvotes", "Upvotes")
PostsForm.DataGridViewPosts.Columns.Add("PostUpvoteRatio", "Upvote Ratio")
PostsForm.DataGridViewPosts.Columns.Add("PostDownvotes", "Downvotes")
PostsForm.DataGridViewPosts.Columns.Add("PostAwards", "Awards")
PostsForm.DataGridViewPosts.Columns.Add("PostTopAward", "Top Award")
PostsForm.DataGridViewPosts.Columns.Add("PostIsCrosspostable", "Is Crosspostable?")
PostsForm.DataGridViewPosts.Columns.Add("PostScore", "Score")
PostsForm.DataGridViewPosts.Columns.Add("PostText", "Text")
PostsForm.DataGridViewPosts.Columns.Add("PostCategory", "Category")
PostsForm.DataGridViewPosts.Columns.Add("PostDomain", "Domain")
PostsForm.DataGridViewPosts.Columns.Add("PostPermalink", "Permalink")
PostsForm.DataGridViewPosts.Columns.Add("PostCreatedAt", "Created At")
PostsForm.DataGridViewPosts.Columns.Add("PostApprovedAt", "Approved At")
PostsForm.DataGridViewPosts.Columns.Add("PostApprovedBy", "Approved By")
End Sub
Public Shared Sub AddRow(dataGridView As DataGridView, post As JObject, postNumber As Integer)
''' <summary>
''' Adds a row to the DataGridView based on the data from a Reddit post.
''' </summary>
''' <param name="dataGridView">The DataGridView to which the row will be added.</param>
''' <param name="post">A JObject representing the Reddit post.</param>
''' <param name="postNumber">The number of the post.</param>
PostsForm.DataGridViewPosts.Rows.Add(postNumber,
post("data")("author"),
post("data")("id"),
post("data")("subreddit_name_prefixed"),
post("data")("subreddit_type"),
post("data")("thumbnail"),
post("data")("over_18"),
post("data")("gilded"),
post("data")("ups"),
post("data")("upvote_ratio"),
post("data")("downs"),
post("data")("total_awards_received"),
post("data")("top_awarded_type"),
post("data")("is_crosspostable"),
post("data")("score"),
post("data")("selftext"),
post("data")("category"),
post("data")("domain"),
post("data")("permalink"),
post("data")("created"),
post("data")("approved_at_utc"),
post("data")("approved_by"))
End Sub
End Class

84
RPST GUI/RPST/DeveloperForm.Designer.vb generated Normal file
View File

@@ -0,0 +1,84 @@
<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()>
Partial Class DeveloperForm
Inherits System.Windows.Forms.Form
'Form overrides dispose to clean up the component list.
<System.Diagnostics.DebuggerNonUserCode()>
Protected Overrides Sub Dispose(ByVal disposing As Boolean)
Try
If disposing AndAlso components IsNot Nothing Then
components.Dispose()
End If
Finally
MyBase.Dispose(disposing)
End Try
End Sub
'Required by the Windows Form Designer
Private components As System.ComponentModel.IContainer
'NOTE: The following procedure is required by the Windows Form Designer
'It can be modified using the Windows Form Designer.
'Do not modify it using the code editor.
<System.Diagnostics.DebuggerStepThrough()>
Private Sub InitializeComponent()
Dim resources As ComponentModel.ComponentResourceManager = New ComponentModel.ComponentResourceManager(GetType(DeveloperForm))
AboutMeLinkLabel = New LinkLabel()
BuyMeACoffeeLinkLabel = New LinkLabel()
GreetingLabel = New Label()
SuspendLayout()
'
' AboutMeLinkLabel
'
AboutMeLinkLabel.AutoSize = True
AboutMeLinkLabel.BackColor = Color.White
AboutMeLinkLabel.Location = New Point(33, 426)
AboutMeLinkLabel.Name = "AboutMeLinkLabel"
AboutMeLinkLabel.Size = New Size(60, 15)
AboutMeLinkLabel.TabIndex = 0
AboutMeLinkLabel.TabStop = True
AboutMeLinkLabel.Text = "About.me"'
' BuyMeACoffeeLinkLabel
'
BuyMeACoffeeLinkLabel.AutoSize = True
BuyMeACoffeeLinkLabel.Location = New Point(33, 451)
BuyMeACoffeeLinkLabel.Name = "BuyMeACoffeeLinkLabel"
BuyMeACoffeeLinkLabel.Size = New Size(96, 15)
BuyMeACoffeeLinkLabel.TabIndex = 1
BuyMeACoffeeLinkLabel.TabStop = True
BuyMeACoffeeLinkLabel.Text = "Buy Me A Coffee"'
' GreetingLabel
'
GreetingLabel.AutoSize = True
GreetingLabel.Font = New Font("Verdana", 27.75F, FontStyle.Bold, GraphicsUnit.Point)
GreetingLabel.Location = New Point(62, 22)
GreetingLabel.Name = "GreetingLabel"
GreetingLabel.Size = New Size(382, 45)
GreetingLabel.TabIndex = 3
GreetingLabel.Text = "Hello, I'm Ritchie"'
' DeveloperForm
'
AutoScaleDimensions = New SizeF(7F, 15F)
AutoScaleMode = AutoScaleMode.Font
BackgroundImage = CType(resources.GetObject("$this.BackgroundImage"), Image)
ClientSize = New Size(510, 510)
Controls.Add(BuyMeACoffeeLinkLabel)
Controls.Add(AboutMeLinkLabel)
Controls.Add(GreetingLabel)
FormBorderStyle = FormBorderStyle.FixedSingle
MaximizeBox = False
MinimizeBox = False
Name = "DeveloperForm"
ShowIcon = False
ShowInTaskbar = False
StartPosition = FormStartPosition.CenterParent
Text = "Developer"
ResumeLayout(False)
PerformLayout()
End Sub
Friend WithEvents AboutMeLinkLabel As LinkLabel
Friend WithEvents BuyMeACoffeeLinkLabel As LinkLabel
Friend WithEvents PictureBox1 As PictureBox
Friend WithEvents GreetingLabel As Label
End Class

View File

@@ -12,6 +12,6 @@
End Sub
Private Sub BuyMeACoffeeLinkLabel_LinkClicked(sender As Object, e As LinkLabelLinkClickedEventArgs) Handles BuyMeACoffeeLinkLabel.LinkClicked
Shell("cmd /c start https://buymeacoffee.com/189381184")
Shell("cmd /c start https://buymeacoffee.com/_rly0nheart")
End Sub
End Class

View File

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

View File

@@ -39,7 +39,7 @@ Namespace My.Resources
Friend ReadOnly Property ResourceManager() As Global.System.Resources.ResourceManager
Get
If Object.ReferenceEquals(resourceMan, Nothing) Then
Dim temp As Global.System.Resources.ResourceManager = New Global.System.Resources.ResourceManager("Reddit_Post_Scraping_Tool.Resources", GetType(Resources).Assembly)
Dim temp As Global.System.Resources.ResourceManager = New Global.System.Resources.ResourceManager("RPST.Resources", GetType(Resources).Assembly)
resourceMan = temp
End If
Return resourceMan

57
RPST GUI/RPST/PostsForm.Designer.vb generated Normal file
View File

@@ -0,0 +1,57 @@
<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()> _
Partial Class PostsForm
Inherits System.Windows.Forms.Form
'Form overrides dispose to clean up the component list.
<System.Diagnostics.DebuggerNonUserCode()> _
Protected Overrides Sub Dispose(ByVal disposing As Boolean)
Try
If disposing AndAlso components IsNot Nothing Then
components.Dispose()
End If
Finally
MyBase.Dispose(disposing)
End Try
End Sub
'Required by the Windows Form Designer
Private components As System.ComponentModel.IContainer
'NOTE: The following procedure is required by the Windows Form Designer
'It can be modified using the Windows Form Designer.
'Do not modify it using the code editor.
<System.Diagnostics.DebuggerStepThrough()> _
Private Sub InitializeComponent()
DataGridViewPosts = New DataGridView()
CType(DataGridViewPosts, ComponentModel.ISupportInitialize).BeginInit()
SuspendLayout()
'
' DataGridViewPosts
'
DataGridViewPosts.BackgroundColor = Color.Gainsboro
DataGridViewPosts.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize
DataGridViewPosts.Dock = DockStyle.Fill
DataGridViewPosts.Location = New Point(0, 0)
DataGridViewPosts.Name = "DataGridViewPosts"
DataGridViewPosts.ReadOnly = True
DataGridViewPosts.RowHeadersVisible = False
DataGridViewPosts.RowTemplate.Height = 25
DataGridViewPosts.Size = New Size(800, 450)
DataGridViewPosts.TabIndex = 3
'
' PostsForm
'
AutoScaleDimensions = New SizeF(7F, 15F)
AutoScaleMode = AutoScaleMode.Font
ClientSize = New Size(800, 450)
Controls.Add(DataGridViewPosts)
Name = "PostsForm"
ShowIcon = False
ShowInTaskbar = False
Text = "PostsForm"
CType(DataGridViewPosts, ComponentModel.ISupportInitialize).EndInit()
ResumeLayout(False)
End Sub
Friend WithEvents DataGridViewPosts As DataGridView
End Class

View File

@@ -0,0 +1,29 @@
Imports Newtonsoft.Json.Linq
Public Class PostsProcessor
Private ApiHandler As New ApiHandler
''' <summary>
''' Fetches Reddit posts based on the given parameters and returns them as a JObject.
''' </summary>
''' <param name="subreddit">The subreddit to fetch posts from.</param>
''' <param name="listing">The type of listing (e.g., "new", "top", etc.).</param>
''' <param name="limit">The maximum number of posts to fetch.</param>
''' <param name="timeframe">The timeframe to consider for the posts (e.g., "day", "week", "month", "year", "all").</param>
''' <returns>A JObject containing the fetched Reddit posts.</returns>
Public Function FetchPosts(subreddit As String, listing As String, limit As Integer, timeframe As String) As JObject
Dim posts As JObject = ApiHandler.ScrapeReddit(subreddit, listing, limit, timeframe)
Return posts
End Function
''' <summary>
''' Checks if the given Reddit post contains the given keyword in its text.
''' </summary>
''' <param name="post">The Reddit post to check.</param>
''' <param name="keyword">The keyword to check for.</param>
''' <returns>True if the post contains the keyword, False otherwise.</returns>
Public Shared Function PostContainsKeyword(post As JObject, keyword As String) As Boolean
Return post("data")("selftext").ToString.ToLower(System.Globalization.CultureInfo.InvariantCulture).Contains(keyword.ToLower(System.Globalization.CultureInfo.InvariantCulture))
End Function
End Class

View File

@@ -3,23 +3,29 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0-windows</TargetFramework>
<StartupObject>Reddit_Post_Scraping_Tool.My.MyApplication</StartupObject>
<StartupObject>RPST.My.MyApplication</StartupObject>
<UseWindowsForms>true</UseWindowsForms>
<MyType>WindowsForms</MyType>
<ApplicationIcon>icon.ico</ApplicationIcon>
<Company>Richard Mwewa</Company>
<Description>Given a subreddit name and a keyword, this program returns all top (by default) posts that contain the specified keyword. </Description>
<Copyright>Copyright (c) 2023 Richard Mwewa. All rights reserved.</Copyright>
<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>
<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.3.0.1</AssemblyVersion>
<FileVersion>1.3.0.1</FileVersion>
<AssemblyVersion>1.4.0.0</AssemblyVersion>
<FileVersion>1.4.0.0</FileVersion>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>
<Version>1.3.0</Version>
<Version>1.4.0</Version>
<PackageTags>reddit;scraper;reddit-scraper;osint</PackageTags>
<PackageReleaseNotes></PackageReleaseNotes>
<AnalysisLevel>6.0-recommended</AnalysisLevel>
<PackageId>RPST (Reddit Post Scraping Tool)</PackageId>
<Authors>Richard Mwewa</Authors>
<NeutralLanguage>en</NeutralLanguage>
<Product>$(AssemblyName)</Product>
<AssemblyName>RPST (Reddit Post Scraping Tool)</AssemblyName>
</PropertyGroup>
<ItemGroup>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Compile Update="AboutForm.vb">
<Compile Update="AboutBox.vb">
<SubType>Form</SubType>
</Compile>
<Compile Update="DeveloperForm.vb">

111
RPST GUI/RPST/Settings.vb Normal file
View File

@@ -0,0 +1,111 @@
Imports Newtonsoft.Json
Imports Newtonsoft.Json.Linq
Imports System.IO
Imports System.Runtime
Imports System.Text.Json
Public Class SettingsManager
Public Property DarkMode As Boolean
Private ReadOnly settingsFilePath As String = Path.Combine(Environment.CurrentDirectory, "settings.json")
' Load settings from the 'settings.json' file
Public Sub LoadSettings()
' Check if the settings.json file exists
' and load the configurations from it
If File.Exists(settingsFilePath) Then
Dim json As String = File.ReadAllText(settingsFilePath)
Dim options As New JsonSerializerOptions With {.PropertyNameCaseInsensitive = True}
Dim settings = Text.Json.JsonSerializer.Deserialize(Of SettingsManager)(json, options)
Me.DarkMode = settings.DarkMode
StartForm.DarkModeToolStripMenuItem.Checked = settings.DarkMode
Else
' Settings file does not exist
' Create a new file with default settings 'False'
Dim defaultSettings = New SettingsManager With {.DarkMode = False}
Dim jsonOutput = Text.Json.JsonSerializer.Serialize(defaultSettings)
File.WriteAllText(settingsFilePath, jsonOutput)
Me.DarkMode = False
StartForm.DarkModeToolStripMenuItem.Checked = False
End If
End Sub
' Toggle Dark mode
Public Sub ToggleDarkMode(enabled As Boolean)
Dim json As String = File.ReadAllText(settingsFilePath)
Dim options As New JsonSerializerOptions With {.PropertyNameCaseInsensitive = True}
Dim settings As SettingsManager = Text.Json.JsonSerializer.Deserialize(Of SettingsManager)(json, options)
settings.DarkMode = enabled
SaveSettings(settings)
ApplyTheme()
End Sub
' Save current settings to settings.json
Private Sub SaveSettings(settings)
Dim jsonOutput = Text.Json.JsonSerializer.Serialize(settings)
File.WriteAllText(settingsFilePath, jsonOutput)
End Sub
' Apply theme
Public Sub ApplyTheme()
Dim DarkMode As Boolean = GetDarkMode()
If DarkMode Then
StartForm.BackColor = ColorTranslator.FromHtml("#FF121212")
StartForm.ToolsToolStripMenuTools.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
StartForm.KeywordTextBox.BackColor = ColorTranslator.FromHtml("#FF2E2E2E")
StartForm.KeywordTextBox.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
StartForm.SubredditTextBox.BackColor = ColorTranslator.FromHtml("#FF2E2E2E")
StartForm.SubredditTextBox.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
StartForm.LimitNumericUpDown.BackColor = ColorTranslator.FromHtml("#FF2E2E2E")
StartForm.LimitNumericUpDown.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
StartForm.LimitNumericUpDown.BackColor = ColorTranslator.FromHtml("#FF2E2E2E")
StartForm.LimitNumericUpDown.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
StartForm.ListingComboBox.BackColor = ColorTranslator.FromHtml("#FF2E2E2E")
StartForm.ListingComboBox.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
StartForm.TimeframeComboBox.BackColor = ColorTranslator.FromHtml("#FF2E2E2E")
StartForm.TimeframeComboBox.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
StartForm.Label1.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
StartForm.Label2.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
StartForm.Label3.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
StartForm.Label4.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
StartForm.Label5.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
Else
StartForm.BackColor = ColorTranslator.FromHtml("#FFFFFFFF")
StartForm.ToolsToolStripMenuTools.ForeColor = ColorTranslator.FromHtml("#FF121212")
StartForm.KeywordTextBox.BackColor = ColorTranslator.FromHtml("#FFFFFFFF")
StartForm.KeywordTextBox.ForeColor = ColorTranslator.FromHtml("#FF121212")
StartForm.SubredditTextBox.BackColor = ColorTranslator.FromHtml("#FFFFFFFF")
StartForm.SubredditTextBox.ForeColor = ColorTranslator.FromHtml("#FF121212")
StartForm.LimitNumericUpDown.BackColor = ColorTranslator.FromHtml("#FFFFFFFF")
StartForm.LimitNumericUpDown.ForeColor = ColorTranslator.FromHtml("#FF121212")
StartForm.LimitNumericUpDown.BackColor = ColorTranslator.FromHtml("#FFFFFFFF")
StartForm.LimitNumericUpDown.ForeColor = ColorTranslator.FromHtml("#FF121212")
StartForm.ListingComboBox.BackColor = ColorTranslator.FromHtml("#FFFFFFFF")
StartForm.ListingComboBox.ForeColor = ColorTranslator.FromHtml("#FF121212")
StartForm.TimeframeComboBox.BackColor = ColorTranslator.FromHtml("#FFFFFFFF")
StartForm.TimeframeComboBox.ForeColor = ColorTranslator.FromHtml("#FF121212")
StartForm.Label1.ForeColor = ColorTranslator.FromHtml("#FF121212")
StartForm.Label2.ForeColor = ColorTranslator.FromHtml("#FF121212")
StartForm.Label3.ForeColor = ColorTranslator.FromHtml("#FF121212")
StartForm.Label4.ForeColor = ColorTranslator.FromHtml("#FF121212")
StartForm.Label5.ForeColor = ColorTranslator.FromHtml("#FF121212")
End If
End Sub
' Get dark mode state from settings.json's key 'DarkMode'
Private Function GetDarkMode() As Boolean
If File.Exists(settingsFilePath) Then
Dim json As String = File.ReadAllText(settingsFilePath)
Dim settings As JObject = JObject.Parse(json)
Return settings("DarkMode").ToObject(Of Boolean)()
Else
Return False
End If
End Function
End Class

View File

@@ -35,17 +35,19 @@ Partial Class StartForm
Label4 = New Label()
Label5 = New Label()
ContextMenuStrip1 = New ContextMenuStrip(components)
SaveResultsJSONToolStripMenuItem = New ToolStripMenuItem()
SaveResultsStripMenuItem = New ToolStripMenuItem()
JSONToolStripMenuItem = New ToolStripMenuItem()
CSVToolStripMenuItem = New ToolStripMenuItem()
DarkModeToolStripMenuItem = New ToolStripMenuItem()
FileMenuStrip = New MenuStrip()
ToolsToolStripMenuTools = New ToolStripMenuItem()
AboutToolStripMenuItem = New ToolStripMenuItem()
DeveloperToolStripMenuItem = New ToolStripMenuItem()
ChekUpdatesToolStripMenuItem = New ToolStripMenuItem()
CheckUpdatesToolStripMenuItem = New ToolStripMenuItem()
ToolStripSeparator2 = New ToolStripSeparator()
QuitToolStripMenuItem = New ToolStripMenuItem()
LimitNumericUpDown = New NumericUpDown()
ToolTip1 = New ToolTip(components)
ContextMenuStrip1.SuspendLayout()
FileMenuStrip.SuspendLayout()
CType(LimitNumericUpDown, ComponentModel.ISupportInitialize).BeginInit()
@@ -86,7 +88,8 @@ Partial Class StartForm
TimeframeComboBox.Name = "TimeframeComboBox"
TimeframeComboBox.Size = New Size(100, 23)
TimeframeComboBox.TabIndex = 8
TimeframeComboBox.Text = "All"'
TimeframeComboBox.Text = "All"
'
' ListingComboBox
'
ListingComboBox.FormattingEnabled = True
@@ -95,7 +98,8 @@ Partial Class StartForm
ListingComboBox.Name = "ListingComboBox"
ListingComboBox.Size = New Size(100, 23)
ListingComboBox.TabIndex = 9
ListingComboBox.Text = "Top"'
ListingComboBox.Text = "Top"
'
' Label1
'
Label1.AutoEllipsis = True
@@ -105,7 +109,8 @@ Partial Class StartForm
Label1.Name = "Label1"
Label1.Size = New Size(56, 23)
Label1.TabIndex = 10
Label1.Text = "Keyword"'
Label1.Text = "Keyword"
'
' Label2
'
Label2.AutoEllipsis = True
@@ -115,7 +120,8 @@ Partial Class StartForm
Label2.Name = "Label2"
Label2.Size = New Size(63, 23)
Label2.TabIndex = 11
Label2.Text = "Subreddit"'
Label2.Text = "Subreddit"
'
' Label3
'
Label3.AutoEllipsis = True
@@ -125,7 +131,8 @@ Partial Class StartForm
Label3.Name = "Label3"
Label3.Size = New Size(56, 23)
Label3.TabIndex = 12
Label3.Text = "Limit"'
Label3.Text = "Limit"
'
' Label4
'
Label4.AutoEllipsis = True
@@ -135,7 +142,8 @@ Partial Class StartForm
Label4.Name = "Label4"
Label4.Size = New Size(56, 23)
Label4.TabIndex = 13
Label4.Text = "Listing"'
Label4.Text = "Listing"
'
' Label5
'
Label5.AutoEllipsis = True
@@ -145,22 +153,23 @@ Partial Class StartForm
Label5.Name = "Label5"
Label5.Size = New Size(71, 23)
Label5.TabIndex = 14
Label5.Text = "Timeframe"'
Label5.Text = "Timeframe"
'
' ContextMenuStrip1
'
ContextMenuStrip1.Items.AddRange(New ToolStripItem() {SaveResultsJSONToolStripMenuItem})
ContextMenuStrip1.Items.AddRange(New ToolStripItem() {SaveResultsStripMenuItem, DarkModeToolStripMenuItem})
ContextMenuStrip1.Name = "ContextMenuStrip1"
ContextMenuStrip1.Size = New Size(144, 26)
ContextMenuStrip1.Size = New Size(144, 48)
'
' SaveResultsJSONToolStripMenuItem
' SaveResultsStripMenuItem
'
SaveResultsStripMenuItem.AutoToolTip = True
SaveResultsStripMenuItem.DropDownItems.AddRange(New ToolStripItem() {JSONToolStripMenuItem, CSVToolStripMenuItem})
SaveResultsStripMenuItem.Image = CType(resources.GetObject("SaveResultsStripMenuItem.Image"), Image)
SaveResultsStripMenuItem.Name = "SaveResultsStripMenuItem"
SaveResultsStripMenuItem.Size = New Size(143, 22)
SaveResultsStripMenuItem.Text = "Save posts to"
'
SaveResultsJSONToolStripMenuItem.AutoToolTip = True
SaveResultsJSONToolStripMenuItem.DropDownItems.AddRange(New ToolStripItem() {JSONToolStripMenuItem, CSVToolStripMenuItem})
SaveResultsJSONToolStripMenuItem.Image = CType(resources.GetObject("SaveResultsJSONToolStripMenuItem.Image"), Image)
SaveResultsJSONToolStripMenuItem.Name = "SaveResultsJSONToolStripMenuItem"
SaveResultsJSONToolStripMenuItem.Size = New Size(143, 22)
SaveResultsJSONToolStripMenuItem.Text = "Save posts to"
SaveResultsJSONToolStripMenuItem.ToolTipText = "Save results to a JSON file"'
' JSONToolStripMenuItem
'
JSONToolStripMenuItem.AutoToolTip = True
@@ -168,7 +177,8 @@ Partial Class StartForm
JSONToolStripMenuItem.Image = CType(resources.GetObject("JSONToolStripMenuItem.Image"), Image)
JSONToolStripMenuItem.Name = "JSONToolStripMenuItem"
JSONToolStripMenuItem.Size = New Size(185, 22)
JSONToolStripMenuItem.Text = "JSON"'
JSONToolStripMenuItem.Text = "JSON"
'
' CSVToolStripMenuItem
'
CSVToolStripMenuItem.AutoToolTip = True
@@ -176,7 +186,17 @@ Partial Class StartForm
CSVToolStripMenuItem.Image = CType(resources.GetObject("CSVToolStripMenuItem.Image"), Image)
CSVToolStripMenuItem.Name = "CSVToolStripMenuItem"
CSVToolStripMenuItem.Size = New Size(185, 22)
CSVToolStripMenuItem.Text = "CSV (coming soon...)"'
CSVToolStripMenuItem.Text = "CSV (coming soon...)"
'
' DarkModeToolStripMenuItem
'
DarkModeToolStripMenuItem.AutoToolTip = True
DarkModeToolStripMenuItem.CheckOnClick = True
DarkModeToolStripMenuItem.Image = CType(resources.GetObject("DarkModeToolStripMenuItem.Image"), Image)
DarkModeToolStripMenuItem.Name = "DarkModeToolStripMenuItem"
DarkModeToolStripMenuItem.Size = New Size(143, 22)
DarkModeToolStripMenuItem.Text = "Dark mode"
'
' FileMenuStrip
'
FileMenuStrip.BackColor = Color.Transparent
@@ -185,58 +205,63 @@ Partial Class StartForm
FileMenuStrip.Name = "FileMenuStrip"
FileMenuStrip.Size = New Size(355, 24)
FileMenuStrip.TabIndex = 0
FileMenuStrip.Text = "MenuStrip1"'
FileMenuStrip.Text = "MenuStrip1"
'
' ToolsToolStripMenuTools
'
ToolsToolStripMenuTools.DropDownItems.AddRange(New ToolStripItem() {AboutToolStripMenuItem, DeveloperToolStripMenuItem, ChekUpdatesToolStripMenuItem, ToolStripSeparator2, QuitToolStripMenuItem})
ToolsToolStripMenuTools.DropDownItems.AddRange(New ToolStripItem() {AboutToolStripMenuItem, DeveloperToolStripMenuItem, CheckUpdatesToolStripMenuItem, ToolStripSeparator2, QuitToolStripMenuItem})
ToolsToolStripMenuTools.Font = New Font("Segoe UI", 9F, FontStyle.Regular, GraphicsUnit.Point)
ToolsToolStripMenuTools.ForeColor = Color.White
ToolsToolStripMenuTools.Image = CType(resources.GetObject("ToolsToolStripMenuTools.Image"), Image)
ToolsToolStripMenuTools.Name = "ToolsToolStripMenuTools"
ToolsToolStripMenuTools.Size = New Size(53, 20)
ToolsToolStripMenuTools.Text = "File"'
ToolsToolStripMenuTools.Size = New Size(28, 20)
'
' AboutToolStripMenuItem
'
AboutToolStripMenuItem.AutoToolTip = True
AboutToolStripMenuItem.Image = CType(resources.GetObject("AboutToolStripMenuItem.Image"), Image)
AboutToolStripMenuItem.Name = "AboutToolStripMenuItem"
AboutToolStripMenuItem.Size = New Size(152, 22)
AboutToolStripMenuItem.Text = "About"'
AboutToolStripMenuItem.Size = New Size(180, 22)
AboutToolStripMenuItem.Text = "About"
'
' DeveloperToolStripMenuItem
'
DeveloperToolStripMenuItem.AutoToolTip = True
DeveloperToolStripMenuItem.Image = CType(resources.GetObject("DeveloperToolStripMenuItem.Image"), Image)
DeveloperToolStripMenuItem.Name = "DeveloperToolStripMenuItem"
DeveloperToolStripMenuItem.Size = New Size(152, 22)
DeveloperToolStripMenuItem.Text = "Developer"'
' ChekUpdatesToolStripMenuItem
DeveloperToolStripMenuItem.Size = New Size(180, 22)
DeveloperToolStripMenuItem.Text = "Developer"
'
' CheckUpdatesToolStripMenuItem
'
CheckUpdatesToolStripMenuItem.AutoToolTip = True
CheckUpdatesToolStripMenuItem.Image = CType(resources.GetObject("CheckUpdatesToolStripMenuItem.Image"), Image)
CheckUpdatesToolStripMenuItem.Name = "CheckUpdatesToolStripMenuItem"
CheckUpdatesToolStripMenuItem.Size = New Size(180, 22)
CheckUpdatesToolStripMenuItem.Text = "Check updates"
'
ChekUpdatesToolStripMenuItem.AutoToolTip = True
ChekUpdatesToolStripMenuItem.Image = CType(resources.GetObject("ChekUpdatesToolStripMenuItem.Image"), Image)
ChekUpdatesToolStripMenuItem.Name = "ChekUpdatesToolStripMenuItem"
ChekUpdatesToolStripMenuItem.Size = New Size(152, 22)
ChekUpdatesToolStripMenuItem.Text = "Check updates"'
' ToolStripSeparator2
'
ToolStripSeparator2.Name = "ToolStripSeparator2"
ToolStripSeparator2.Size = New Size(149, 6)
ToolStripSeparator2.Size = New Size(177, 6)
'
' QuitToolStripMenuItem
'
QuitToolStripMenuItem.AutoToolTip = True
QuitToolStripMenuItem.Image = CType(resources.GetObject("QuitToolStripMenuItem.Image"), Image)
QuitToolStripMenuItem.Name = "QuitToolStripMenuItem"
QuitToolStripMenuItem.Size = New Size(152, 22)
QuitToolStripMenuItem.Text = "Quit"'
QuitToolStripMenuItem.Size = New Size(180, 22)
QuitToolStripMenuItem.Text = "Quit"
'
' LimitNumericUpDown
'
LimitNumericUpDown.Location = New Point(89, 125)
LimitNumericUpDown.Minimum = New [Decimal](New Integer() {5, 0, 0, 0})
LimitNumericUpDown.Minimum = New Decimal(New Integer() {5, 0, 0, 0})
LimitNumericUpDown.Name = "LimitNumericUpDown"
LimitNumericUpDown.ReadOnly = True
LimitNumericUpDown.Size = New Size(100, 23)
LimitNumericUpDown.TabIndex = 15
LimitNumericUpDown.Value = New [Decimal](New Integer() {5, 0, 0, 0})
LimitNumericUpDown.Value = New Decimal(New Integer() {10, 0, 0, 0})
'
' StartForm
'
@@ -263,7 +288,7 @@ Partial Class StartForm
MaximizeBox = False
Name = "StartForm"
StartPosition = FormStartPosition.CenterScreen
Text = "Reddit Post Scraping Tool"
Text = "ProgramName ProgramVersion"
ContextMenuStrip1.ResumeLayout(False)
FileMenuStrip.ResumeLayout(False)
FileMenuStrip.PerformLayout()
@@ -289,9 +314,11 @@ Partial Class StartForm
Friend WithEvents DeveloperToolStripMenuItem As ToolStripMenuItem
Friend WithEvents ToolStripSeparator2 As ToolStripSeparator
Friend WithEvents QuitToolStripMenuItem As ToolStripMenuItem
Friend WithEvents SaveResultsJSONToolStripMenuItem As ToolStripMenuItem
Friend WithEvents ChekUpdatesToolStripMenuItem As ToolStripMenuItem
Friend WithEvents SaveResultsStripMenuItem As ToolStripMenuItem
Friend WithEvents CheckUpdatesToolStripMenuItem As ToolStripMenuItem
Friend WithEvents JSONToolStripMenuItem As ToolStripMenuItem
Friend WithEvents CSVToolStripMenuItem As ToolStripMenuItem
Friend WithEvents LimitNumericUpDown As NumericUpDown
Friend WithEvents DarkModeToolStripMenuItem As ToolStripMenuItem
Friend WithEvents ToolTip1 As ToolTip
End Class

View File

@@ -1,4 +1,64 @@
<root>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing"">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
@@ -61,7 +121,7 @@
<value>132, 17</value>
</metadata>
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
<data name="SaveResultsJSONToolStripMenuItem.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<data name="SaveResultsStripMenuItem.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAYAAAD0eNT6AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO
vAAADrwBlbxySQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACoYSURBVHhe7d0L
@@ -574,6 +634,268 @@
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJbvZS/7
/4YzHMz4V1N2AAAAAElFTkSuQmCC
</value>
</data>
<data name="DarkModeToolStripMenuItem.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
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
</value>
</data>
<metadata name="FileMenuStrip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
@@ -1425,7 +1747,7 @@
RK5CYII=
</value>
</data>
<data name="ChekUpdatesToolStripMenuItem.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<data name="CheckUpdatesToolStripMenuItem.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAYAAAD0eNT6AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAL
FQAACxUBgJnYgwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAGmYSURBVHhe7Z0J
@@ -1893,6 +2215,9 @@
AY7pF0Fg9EWkAAAAAElFTkSuQmCC
</value>
</data>
<metadata name="ToolTip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>289, 17</value>
</metadata>
<data name="$this.Icon" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
AAABAAEAAAAAAAEAIACvPgAAFgAAAIlQTkcNChoKAAAADUlIRFIAAAEAAAABAAgGAAAAXHKoZgAAPnZJ

105
RPST GUI/RPST/StartForm.vb Normal file
View File

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

180
RPST GUI/RPST/Utilities.vb Normal file
View File

@@ -0,0 +1,180 @@
Imports System.IO
Imports Newtonsoft.Json
Imports Newtonsoft.Json.Linq
Public Class Utilities
''' <summary>
''' Collects user inputs, fetches Reddit posts based on the inputs, checks if posts contain the keyword, and saves posts to a JSON file if necessary.
''' </summary>
''' <param name="JSONToolStripMenuItem">Indicates whether to save the posts to a JSON file.</param>
''' <remarks>
''' This function initializes the DataGridView, iterates over each post, adds the posts containing the keyword to the DataGridView and updates the UI.
''' It also shows a message if the keyword was not found in any of the posts or if the inputs are empty.
''' </remarks>
Public Shared Sub ProcessRedditPosts(JSONToolStripMenuItem As ToolStripMenuItem)
' Collect inputs from the user
Dim inputs = CollectInputs()
If inputs.HasValue Then
' Initialize the DataGridView
DataGridViewHandler.AddColumn(PostsForm.DataGridViewPosts)
' Fetch Reddit posts based on the inputs
Dim processor As New PostsProcessor()
Dim posts As JObject = processor.FetchPosts(inputs.Value.Subreddit, inputs.Value.Listing, inputs.Value.Limit, inputs.Value.Timeframe)
Dim totalPosts As Integer = 0
Dim keywordFound As Boolean = False
' Iterate over each post
For Each post In posts("data")("children")
totalPosts += 1
' Check if the post contains the keyword
If PostsProcessor.PostContainsKeyword(post, inputs.Value.Keyword.ToLower(System.Globalization.CultureInfo.InvariantCulture)) Then
' Add the post to the DataGridView
DataGridViewHandler.AddRow(PostsForm.DataGridViewPosts, post, totalPosts)
PostsForm.Show()
keywordFound = True
End If
Next
' Check if the keyword was found in any posts
If Not keywordFound Then
MessageBox.Show($"Keyword `{inputs.Value.Keyword}` was not found in any of the " + posts("data")("children").Count.ToString(System.Globalization.CultureInfo.InvariantCulture) _
+ $" {inputs.Value.Listing} posts from r/{inputs.Value.Subreddit}", "Not Found", MessageBoxButtons.OK, MessageBoxIcon.Warning)
End If
If JSONToolStripMenuItem.Checked Then
' Save posts to a JSON file if the JSONToolStripMenuItem is checked
Utilities.SavePostsToJson(posts("data"))
End If
Else
MessageBox.Show("Inputs cannot be empty. Please enter a keyword and a subreddit.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
End If
End Sub
''' <summary>
''' Checks for the existence of the 'logs' directory under the 'RPST' directory within the user's AppData\Roaming folder.
''' If the directory does not exist, it creates one.
''' </summary>
''' <remarks>
''' The directory path is 'C:\Users\<username>\AppData\Roaming\RPST\logs'.
''' If the 'RPST' or 'logs' directories do not exist, the function will create them.
''' If the directories already exist, the function will not perform any actions.
''' </remarks>
Public Shared Sub PathFinder()
Dim directoryPath As String = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RPST", "logs")
If Not Directory.Exists(directoryPath) Then
Directory.CreateDirectory(directoryPath)
End If
End Sub
''' <summary>
''' Collects and validates user inputs from StartForm and returns them as a Tuple.
''' </summary>
''' <returns>
''' Tuple containing:
''' Keyword (String) - Keyword entered by user in the StartForm.
''' Subreddit (String) - Subreddit entered by user in the StartForm.
''' Listing (String) - Listing chosen by user in the StartForm, defaults to 'top' if none is selected.
''' Limit (Integer) - Limit entered by user in the StartForm, defaults to 10 if the entered value is over 100.
''' Timeframe (String) - Timeframe chosen by user in the StartForm, defaults to 'all' if none is selected.
''' </returns>
''' <remarks>
''' If keyword or subreddit are empty, Displays a warning and returns nothing.
''' </remarks>
Public Shared Function CollectInputs() As (Keyword As String, Subreddit As String, Listing As String, Limit As Integer, Timeframe As String)?
Dim keyword As String = StartForm.KeywordTextBox.Text.Trim()
Dim subreddit As String = StartForm.SubredditTextBox.Text.Trim()
' Convert the Keyword and Subreddit to lowercase using InvariantCulture
Dim listing As String = If(String.IsNullOrEmpty(StartForm.ListingComboBox.Text), "top", StartForm.ListingComboBox.Text.ToLower(System.Globalization.CultureInfo.InvariantCulture).Trim())
Dim limit As Integer = StartForm.LimitNumericUpDown.Value
Dim timeframe As String = If(String.IsNullOrEmpty(StartForm.TimeframeComboBox.Text), "all", StartForm.TimeframeComboBox.Text.ToLower(System.Globalization.CultureInfo.InvariantCulture).Trim())
' Validate inputs
If String.IsNullOrEmpty(keyword) Then
MessageBox.Show("Keyword should not be empty", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning)
Return Nothing
End If
If String.IsNullOrEmpty(subreddit) Then
MessageBox.Show("Subreddit should not be empty", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning)
Return Nothing
End If
If limit > 100 Then
MessageBox.Show("Limit should not be over 100. Defaulting to 10", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning)
limit = 10
End If
Return (keyword, subreddit, listing, limit, timeframe)
End Function
''' <summary>
''' Saves the gives posts' data to a JSON file.
''' </summary>
''' <param name="Posts">The object containing posts to be saved.</param>
''' <remarks>
''' This function allows the user to select a location to save the posts.
''' If the user confirms the save location, the posts will be serialized
''' to JSON with an indented format and written to the chosen file.
''' A success message will be displayed to the user upon successful save.
''' </remarks>
Public Shared Sub SavePostsToJson(Posts As Object)
Dim saveFileDialog As New SaveFileDialog With {
.Filter = "JSON files (*.json)|*.json",
.Title = "Save posts to JSON"
}
If saveFileDialog.ShowDialog() = DialogResult.OK Then
Dim fileName As String = saveFileDialog.FileName
Dim serializerSettings As New JsonSerializerSettings With {
.Formatting = Formatting.Indented
}
Dim json As String = JsonConvert.SerializeObject(Posts, serializerSettings)
File.WriteAllText(fileName, json)
MessageBox.Show($"Posts saved to {fileName}", "Success", MessageBoxButtons.OK, MessageBoxIcon.Information)
End If
End Sub
''' <summary>
''' Shows the license notice in a messagebox.
''' </summary>
''' <remarks>
''' The license text is retrieved from the AboutBox.LicenseText property.
''' The messagebox is displayed with the title "License" and an information icon.
''' </remarks>
Public Shared Sub LicenseNotice()
MessageBox.Show(AboutBox.LicenseText, "License", MessageBoxButtons.OK, MessageBoxIcon.Information)
End Sub
''' <summary>
''' Checks if the "first-launch.log" file exists in the directory: C:\Users\<username>\AppData\Roaming\RedditPostScrapingTool\logs.
''' If the file doesn't exist, it creates one. This file is used to determine whether the program has been run before.
''' If the program is being run for the first time, a license notice will be displayed.
''' </summary>
Public Shared Sub LogFirstTimeLaunch()
Dim filePath As String = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RPST", "logs", "first-launch.log")
Dim textToWrite As String = $"
{My.Application.Info.AssemblyName}
-------------------------
User: {Environment.UserName}
Host: {Environment.MachineName}
OS: {Environment.OSVersion}
x64: {Environment.Is64BitOperatingSystem}
First launched on: {DateTime.Now}"
If Not File.Exists(filePath) Then
LicenseNotice()
File.WriteAllText(filePath, textToWrite)
Else
' DO NOTHING
End If
End Sub
End Class

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -1,14 +0,0 @@
Public Class AboutForm
Private Sub AboutForm_Load(sender As Object, e As EventArgs) Handles MyBase.Load
DescriptionLabel.Text = "Given a subreddit name and a keyword,
Reddit Post Scraping Tool returns all top posts
(by default) that contain the specified keyword."
VersionLabel.Text = $"v{My.Application.Info.Version}"
LicenseRichTextBox.Text = StartForm.LicenseText
End Sub
Private Sub LinkLabel1_LinkClicked(sender As Object, e As LinkLabelLinkClickedEventArgs) Handles WikiLinkLabel.LinkClicked
Shell("cmd /c start https://github.com/bellingcat/reddit-post-scraping-tool/wiki")
End Sub
End Class

View File

@@ -1,59 +0,0 @@
Imports System.IO
Imports System.Net.Http
Imports Newtonsoft.Json
Imports Newtonsoft.Json.Linq
Public Class ApiHandler
Public logfile As String = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RedditPostScrapingTool", "logs", $"debug.log")
Public headers As String = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1.2 Safari/605.1.15"
Public UpdatesEndpoint As String = "https://api.github.com/repos/bellingcat/reddit-post-scraping-tool/releases/latest"
Public Function ScrapeReddit(subreddit, listing, limit, timeframe) As JObject
Dim ApiEndpoint As String = $"https://reddit.com/r/{subreddit}/{listing}.json?limit={limit}&t={timeframe}').json()"
Try
Dim httpClient As New HttpClient()
httpClient.DefaultRequestHeaders.Add("User-Agent", headers)
Dim response As HttpResponseMessage = httpClient.GetAsync(ApiEndpoint).Result
If response.IsSuccessStatusCode Then
Dim json As String = response.Content.ReadAsStringAsync().Result
Dim data As JObject = JsonConvert.DeserializeObject(Of JObject)(json)
Return data
Else
' handle the case when the response status is not successful
' return an empty JObject or throw an exception
Return New JObject()
MessageBox.Show(response.ReasonPhrase, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
End If
Catch ex As Exception
' handle the exception
' return an empty JObject or throw an exception
Return New JObject()
My.Computer.FileSystem.WriteAllText(logfile, $"{DateTime.Now}: {ex}{Environment.NewLine}", True)
MessageBox.Show($"{ex.Message}. Please see the debug log '{logfile}' for more information.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
End Try
Return New JObject()
End Function
' Gets remote version information from the repository release page
Public Function CheckUpdates() As JObject
Try
Dim httpClient As New HttpClient()
httpClient.DefaultRequestHeaders.Add("User-Agent", headers)
Dim response As HttpResponseMessage = httpClient.GetAsync(UpdatesEndpoint).Result
If response.IsSuccessStatusCode Then
Dim json As String = response.Content.ReadAsStringAsync().Result
Dim data As JObject = JsonConvert.DeserializeObject(Of JObject)(json)
Return data
Else
'Return New JObject()
MessageBox.Show(response.ReasonPhrase, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
End If
Catch ex As Exception
'Return New JObject()
My.Computer.FileSystem.WriteAllText(logfile, $"{DateTime.Now}: {ex}{Environment.NewLine}", True)
MessageBox.Show($"{ex.Message}. Please see the debug log '{logfile}' for more information.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
End Try
Return New JObject()
End Function
End Class

View File

@@ -1,87 +0,0 @@
<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()>
Partial Class DeveloperForm
Inherits System.Windows.Forms.Form
'Form overrides dispose to clean up the component list.
<System.Diagnostics.DebuggerNonUserCode()>
Protected Overrides Sub Dispose(ByVal disposing As Boolean)
Try
If disposing AndAlso components IsNot Nothing Then
components.Dispose()
End If
Finally
MyBase.Dispose(disposing)
End Try
End Sub
'Required by the Windows Form Designer
Private components As System.ComponentModel.IContainer
'NOTE: The following procedure is required by the Windows Form Designer
'It can be modified using the Windows Form Designer.
'Do not modify it using the code editor.
<System.Diagnostics.DebuggerStepThrough()>
Private Sub InitializeComponent()
Dim resources As System.ComponentModel.ComponentResourceManager = New System.ComponentModel.ComponentResourceManager(GetType(DeveloperForm))
Me.AboutMeLinkLabel = New System.Windows.Forms.LinkLabel()
Me.BuyMeACoffeeLinkLabel = New System.Windows.Forms.LinkLabel()
Me.GreetingLabel = New System.Windows.Forms.Label()
Me.SuspendLayout()
'
'AboutMeLinkLabel
'
Me.AboutMeLinkLabel.AutoSize = True
Me.AboutMeLinkLabel.BackColor = System.Drawing.Color.White
Me.AboutMeLinkLabel.Location = New System.Drawing.Point(33, 426)
Me.AboutMeLinkLabel.Name = "AboutMeLinkLabel"
Me.AboutMeLinkLabel.Size = New System.Drawing.Size(60, 15)
Me.AboutMeLinkLabel.TabIndex = 0
Me.AboutMeLinkLabel.TabStop = True
Me.AboutMeLinkLabel.Text = "About.me"
'
'BuyMeACoffeeLinkLabel
'
Me.BuyMeACoffeeLinkLabel.AutoSize = True
Me.BuyMeACoffeeLinkLabel.Location = New System.Drawing.Point(33, 451)
Me.BuyMeACoffeeLinkLabel.Name = "BuyMeACoffeeLinkLabel"
Me.BuyMeACoffeeLinkLabel.Size = New System.Drawing.Size(96, 15)
Me.BuyMeACoffeeLinkLabel.TabIndex = 1
Me.BuyMeACoffeeLinkLabel.TabStop = True
Me.BuyMeACoffeeLinkLabel.Text = "Buy Me A Coffee"
'
'GreetingLabel
'
Me.GreetingLabel.AutoSize = True
Me.GreetingLabel.Font = New System.Drawing.Font("Verdana", 27.75!, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point)
Me.GreetingLabel.Location = New System.Drawing.Point(62, 22)
Me.GreetingLabel.Name = "GreetingLabel"
Me.GreetingLabel.Size = New System.Drawing.Size(382, 45)
Me.GreetingLabel.TabIndex = 3
Me.GreetingLabel.Text = "Hello, I'm Ritchie"
'
'DeveloperForm
'
Me.AutoScaleDimensions = New System.Drawing.SizeF(7.0!, 15.0!)
Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font
Me.BackgroundImage = CType(resources.GetObject("$this.BackgroundImage"), System.Drawing.Image)
Me.ClientSize = New System.Drawing.Size(510, 510)
Me.Controls.Add(Me.BuyMeACoffeeLinkLabel)
Me.Controls.Add(Me.AboutMeLinkLabel)
Me.Controls.Add(Me.GreetingLabel)
Me.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle
Me.MaximizeBox = False
Me.Name = "DeveloperForm"
Me.ShowIcon = False
Me.ShowInTaskbar = False
Me.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent
Me.Text = "Developer"
Me.ResumeLayout(False)
Me.PerformLayout()
End Sub
Friend WithEvents AboutMeLinkLabel As LinkLabel
Friend WithEvents BuyMeACoffeeLinkLabel As LinkLabel
Friend WithEvents PictureBox1 As PictureBox
Friend WithEvents GreetingLabel As Label
End Class

View File

@@ -1,56 +0,0 @@
<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()> _
Partial Class PostsForm
Inherits System.Windows.Forms.Form
'Form overrides dispose to clean up the component list.
<System.Diagnostics.DebuggerNonUserCode()> _
Protected Overrides Sub Dispose(ByVal disposing As Boolean)
Try
If disposing AndAlso components IsNot Nothing Then
components.Dispose()
End If
Finally
MyBase.Dispose(disposing)
End Try
End Sub
'Required by the Windows Form Designer
Private components As System.ComponentModel.IContainer
'NOTE: The following procedure is required by the Windows Form Designer
'It can be modified using the Windows Form Designer.
'Do not modify it using the code editor.
<System.Diagnostics.DebuggerStepThrough()> _
Private Sub InitializeComponent()
Me.DataGridViewPosts = New System.Windows.Forms.DataGridView()
CType(Me.DataGridViewPosts, System.ComponentModel.ISupportInitialize).BeginInit()
Me.SuspendLayout()
'
'DataGridViewPosts
'
Me.DataGridViewPosts.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize
Me.DataGridViewPosts.Dock = System.Windows.Forms.DockStyle.Fill
Me.DataGridViewPosts.Location = New System.Drawing.Point(0, 0)
Me.DataGridViewPosts.Name = "DataGridViewPosts"
Me.DataGridViewPosts.ReadOnly = True
Me.DataGridViewPosts.RowTemplate.Height = 25
Me.DataGridViewPosts.Size = New System.Drawing.Size(800, 450)
Me.DataGridViewPosts.TabIndex = 3
'
'PostsForm
'
Me.AutoScaleDimensions = New System.Drawing.SizeF(7.0!, 15.0!)
Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font
Me.ClientSize = New System.Drawing.Size(800, 450)
Me.Controls.Add(Me.DataGridViewPosts)
Me.Name = "PostsForm"
Me.ShowIcon = False
Me.ShowInTaskbar = False
Me.Text = "PostsForm"
CType(Me.DataGridViewPosts, System.ComponentModel.ISupportInitialize).EndInit()
Me.ResumeLayout(False)
End Sub
Friend WithEvents DataGridViewPosts As DataGridView
End Class

View File

@@ -1,283 +0,0 @@
Imports System.IO
Imports Newtonsoft.Json
Imports Newtonsoft.Json.Linq
Public Class StartForm
' Create the program's directory
' We will store information about the user and current machine in it
Public LicenseText As String = "MIT License
Copyright (c) 2023 Richard Mwewa
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the ""Software""), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE."
Private Sub PathFinder()
Dim directoryPath As String = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RedditPostScrapingTool", "logs")
If Not Directory.Exists(directoryPath) Then
Directory.CreateDirectory(directoryPath)
Else
' DO NOTHING
End If
End Sub
Private Sub LicenseNotice()
MessageBox.Show(LicenseText, "License", MessageBoxButtons.OK, MessageBoxIcon.Information)
End Sub
' Create a file in C:\Users\<username>\AppData\Roaming\RedditPostScrapingTool, this will be used to determine
' Whether the program has been run before
' If it has not been run before, display the license notice
Private Sub LogFirstTimeLaunch()
Dim filePath As String = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RedditPostScrapingTool", "logs", "first_launch.log")
Dim textToWrite As String = $"
{My.Application.Info.AssemblyName}
-------------------------
User: {Environment.UserName}
Host: {Environment.MachineName}
OS: {Environment.OSVersion}
x64: {Environment.Is64BitOperatingSystem}
First launched on: {DateTime.Now}"
If Not File.Exists(filePath) Then
LicenseNotice()
File.WriteAllText(filePath, textToWrite)
Else
' DO NOTHING
End If
End Sub
' Check the current time
' add a dark background to the program if it's evening
' This is my way of implementing auto dark-mode (you could help if you know a better way :) )
Private Sub DarkModeProperties()
Dim currentHour As Integer = DateTime.Now.Hour
If currentHour >= 6 And currentHour < 18 Then
Me.BackColor = ColorTranslator.FromHtml("#FFFFFFFF")
ToolsToolStripMenuTools.ForeColor = ColorTranslator.FromHtml("#FF121212")
KeywordTextBox.BackColor = ColorTranslator.FromHtml("#FFFFFFFF")
KeywordTextBox.ForeColor = ColorTranslator.FromHtml("#FF121212")
SubredditTextBox.BackColor = ColorTranslator.FromHtml("#FFFFFFFF")
SubredditTextBox.ForeColor = ColorTranslator.FromHtml("#FF121212")
LimitNumericUpDown.BackColor = ColorTranslator.FromHtml("#FFFFFFFF")
LimitNumericUpDown.ForeColor = ColorTranslator.FromHtml("#FF121212")
LimitNumericUpDown.BackColor = ColorTranslator.FromHtml("#FFFFFFFF")
LimitNumericUpDown.ForeColor = ColorTranslator.FromHtml("#FF121212")
ListingComboBox.BackColor = ColorTranslator.FromHtml("#FFFFFFFF")
ListingComboBox.ForeColor = ColorTranslator.FromHtml("#FF121212")
TimeframeComboBox.BackColor = ColorTranslator.FromHtml("#FFFFFFFF")
TimeframeComboBox.ForeColor = ColorTranslator.FromHtml("#FF121212")
Label1.ForeColor = ColorTranslator.FromHtml("#FF121212")
Label2.ForeColor = ColorTranslator.FromHtml("#FF121212")
Label3.ForeColor = ColorTranslator.FromHtml("#FF121212")
Label4.ForeColor = ColorTranslator.FromHtml("#FF121212")
Label5.ForeColor = ColorTranslator.FromHtml("#FF121212")
Else
Me.BackColor = ColorTranslator.FromHtml("#FF121212")
ToolsToolStripMenuTools.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
KeywordTextBox.BackColor = ColorTranslator.FromHtml("#FF2E2E2E")
KeywordTextBox.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
SubredditTextBox.BackColor = ColorTranslator.FromHtml("#FF2E2E2E")
SubredditTextBox.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
LimitNumericUpDown.BackColor = ColorTranslator.FromHtml("#FF2E2E2E")
LimitNumericUpDown.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
LimitNumericUpDown.BackColor = ColorTranslator.FromHtml("#FF2E2E2E")
LimitNumericUpDown.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
ListingComboBox.BackColor = ColorTranslator.FromHtml("#FF2E2E2E")
ListingComboBox.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
TimeframeComboBox.BackColor = ColorTranslator.FromHtml("#FF2E2E2E")
TimeframeComboBox.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
Label1.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
Label2.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
Label3.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
Label4.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
Label5.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
End If
End Sub
Private Sub ScrapeButton_Click(sender As Object, e As EventArgs) Handles ScrapeButton.Click
Dim ApiHandler As New ApiHandler
Dim Keyword As String = KeywordTextBox.Text
Dim Subreddit As String = SubredditTextBox.Text
Dim Listing As String = ListingComboBox.Text.ToLower()
Dim Limit As Integer = LimitNumericUpDown.Value
Dim Timeframe As String = TimeframeComboBox.Text.ToLower()
Dim FoundPosts As Integer = 0
Dim TotalPosts As Integer = 0
' Clear the Columns and Rows before adding Items to them
PostsForm.DataGridViewPosts.Rows.Clear()
PostsForm.DataGridViewPosts.Columns.Clear()
PostsForm.DataGridViewPosts.Columns.Add("PostCount", "Post Number")
PostsForm.DataGridViewPosts.Columns.Add("PostAuthor", "Author")
PostsForm.DataGridViewPosts.Columns.Add("PostID", "ID")
PostsForm.DataGridViewPosts.Columns.Add("PostSubreddit", "Subreddit")
PostsForm.DataGridViewPosts.Columns.Add("SubredditVisibility", "Subreddit Visibility")
PostsForm.DataGridViewPosts.Columns.Add("PostThumbnail", "Thumbnail")
PostsForm.DataGridViewPosts.Columns.Add("PostIsNSFW", "NSFW")
PostsForm.DataGridViewPosts.Columns.Add("PostIsGilded", "Gilded")
PostsForm.DataGridViewPosts.Columns.Add("PostUpvotes", "Upvotes")
PostsForm.DataGridViewPosts.Columns.Add("PostUpvoteRatio", "Upvote Ratio")
PostsForm.DataGridViewPosts.Columns.Add("PostDownvotes", "Downvotes")
PostsForm.DataGridViewPosts.Columns.Add("PostAwards", "Awards")
PostsForm.DataGridViewPosts.Columns.Add("PostTopAward", "Top Award")
PostsForm.DataGridViewPosts.Columns.Add("PostIsCrosspostable", "Is Crosspostable?")
PostsForm.DataGridViewPosts.Columns.Add("PostScore", "Score")
PostsForm.DataGridViewPosts.Columns.Add("PostText", "Text")
PostsForm.DataGridViewPosts.Columns.Add("PostCategory", "Category")
PostsForm.DataGridViewPosts.Columns.Add("PostDomain", "Domain")
PostsForm.DataGridViewPosts.Columns.Add("PostPermalink", "Permalink")
PostsForm.DataGridViewPosts.Columns.Add("PostCreatedAt", "Created At")
PostsForm.DataGridViewPosts.Columns.Add("PostApprovedAt", "Approved At")
PostsForm.DataGridViewPosts.Columns.Add("PostApprovedBy", "Approved By")
If Limit > 100 Then
MessageBox.Show("Limit should not be over 100. Defaulting to 10", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning)
End If
If Listing = "" Then
Listing = "top"
End If
If Timeframe = "" Then
Timeframe = "all"
End If
If Keyword = "" Then
MessageBox.Show("Keyword should not be emtpy", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning)
ElseIf Subreddit = "" Then
MessageBox.Show("Subreddit should not be emtpy", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning)
Else
PostsForm.Text = $"Reddit Post Scraping Tool - {Keyword}"
Dim Posts As JObject = ApiHandler.ScrapeReddit(Subreddit, Listing, Limit, Timeframe)
For Each Post In Posts("data")("children")
TotalPosts += 1
If Post("data")("selftext").ToString.ToLower().Contains(KeywordTextBox.Text.ToLower()) Then
FoundPosts += 1
PostsForm.DataGridViewPosts.Rows.Add(TotalPosts, Post("data")("author"), Post("data")("id"), Post("data")("subreddit_name_prefixed"),
Post("data")("subreddit_type"), Post("data")("thumbnail"), Post("data")("over_18"), Post("data")("gilded"),
Post("data")("ups"), Post("data")("upvote_ratio"), Post("data")("downs"), Post("data")("total_awards_received"),
Post("data")("top_awarded_type"), Post("data")("is_crosspostable"), Post("data")("score"), Post("data")("selftext"),
Post("data")("category"), Post("data")("domain"), Post("data")("permalink"), Post("data")("created"),
Post("data")("approved_at_utc"), Post("data")("approved_by"))
End If
Next
'Don't show the results form if found posts are not greater than 0
If FoundPosts > 0 Then
MessageBox.Show($"Keyword `{Keyword}` was found in {FoundPosts}/" + Posts("data")("children").Count.ToString _
+ $" {Listing} posts from r/{Subreddit}", "Found", MessageBoxButtons.OK, MessageBoxIcon.Information)
PostsForm.Show()
Else
MessageBox.Show($"Keyword `{Keyword}` was not found in either one of the " + Posts("data")("children").Count.ToString _
+ $" {Listing} posts from r/{Subreddit}", "Not Found", MessageBoxButtons.OK, MessageBoxIcon.Warning)
End If
If JSONToolStripMenuItem.Checked Then
Dim saveFileDialog As New SaveFileDialog()
saveFileDialog.Filter = "JSON files (*.json)|*.json"
saveFileDialog.Title = "Save posts to JSON"
If saveFileDialog.ShowDialog() = DialogResult.OK Then
Dim fileName As String = saveFileDialog.FileName
Dim serializerSettings As New JsonSerializerSettings()
serializerSettings.Formatting = Formatting.Indented
Dim json As String = JsonConvert.SerializeObject(Posts("data"), serializerSettings)
System.IO.File.WriteAllText(fileName, json)
MessageBox.Show($"Results saved to {fileName} successfully!", "Success", MessageBoxButtons.OK, MessageBoxIcon.Information)
End If
End If
End If
End Sub
' StartForm load event
Private Sub StartForm_Load(sender As Object, e As EventArgs) Handles MyBase.Load
PathFinder()
LogFirstTimeLaunch()
DarkModeProperties()
ToolsToolStripMenuTools.Text = Environment.UserName
Me.Text = $"{My.Application.Info.AssemblyName} v{My.Application.Info.Version}"
End Sub
Private Sub AboutToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles AboutToolStripMenuItem.Click
AboutForm.ShowDialog()
End Sub
Private Sub QuitToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles QuitToolStripMenuItem.Click
Dim result As DialogResult = MessageBox.Show("This will close the program, continue?", "Quit", MessageBoxButtons.YesNo, MessageBoxIcon.Question)
If result = DialogResult.Yes Then
Me.Close()
End If
End Sub
Private Sub DeveloperToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles DeveloperToolStripMenuItem.Click
DeveloperForm.ShowDialog()
End Sub
Private Sub ChekUpdatesToolStripMenuItem_Click(sender As Object, e As EventArgs) Handles ChekUpdatesToolStripMenuItem.Click
Dim ApiHandler As New ApiHandler()
Dim data As JObject = ApiHandler.CheckUpdates()
If data("tag_name").ToString = $"{My.Application.Info.Version}" Then
MessageBox.Show($"You're running the current version v{My.Application.Info.Version} of {My.Application.Info.ProductName}. Check again soon! :)", "Information", MessageBoxButtons.OK, MessageBoxIcon.Information)
Else
Dim confirm As DialogResult = MessageBox.Show($"A new version v{data("tag_name")} of {My.Application.Info.ProductName} is availble, would you like to get it?
What's new in v{data("tag_name")}?
{data("body")}
", "Update", MessageBoxButtons.YesNo, MessageBoxIcon.Question)
If confirm = DialogResult.Yes Then
Shell($"cmd /c start https://github.com/bellingcat/reddit-post-scraping-tool/releases/tag/{data("tag_name")}")
End If
End If
End Sub
Private Sub ToolsToolStripMenuTools_Click(sender As Object, e As EventArgs) Handles ToolsToolStripMenuTools.Click
ToolsToolStripMenuTools.ForeColor = ColorTranslator.FromHtml("#FF121212")
End Sub
Private Sub ToolsToolStripMenuTools_DropDownClosed(sender As Object, e As EventArgs) Handles ToolsToolStripMenuTools.DropDownClosed
ToolsToolStripMenuTools.ForeColor = ColorTranslator.FromHtml("#FFFFFFFF")
End Sub
End Class

View File

@@ -2,10 +2,13 @@
requires = ["setuptools", "setuptools-scm"]
build-backend = "setuptools.build_meta"
[tool.setuptools]
packages = ["rpst"]
[project]
name = "reddit-post-scraping-tool"
version = "1.3.0.1"
description = "Given a subreddit name and a keyword, this program returns all top (by default) posts that contain the specified word."
version = "1.4.0.0"
description = "Given a subreddit name and a keyword, RPST returns all top (by default) posts that contain the specified keyword."
readme = "README.md"
requires-python = ">=3.8"
license = {file = "LICENSE"}
@@ -27,9 +30,9 @@ dependencies = [
]
[project.urls]
homepage = "https://github.com/bellingcat/reddit-post-scraping-tool"
homepage = "https://github.com/bellingcat"
documentation = "https://github.com/bellingcat/reddit-post-scraping-tool/wiki"
repository = "https://github.com/bellingcat/reddit-post-scraping-tool.git"
[project.scripts]
reddit_post_scraping_tool = "reddit_post_scraping_tool.main:main"
rpst = "rpst.__main:run"

View File

@@ -1,13 +0,0 @@
from reddit_post_scraping_tool.reddit_post_scraping_tool import *
def main():
try:
check_updates("1.3.0.1")
reddit_post_scraper()
except KeyboardInterrupt:
log.warning(f"User interruption detected.")
except Exception as e:
log.error(e)
finally:
log.info(f'Finished in {datetime.now() - start_time} seconds.')

View File

@@ -1,91 +0,0 @@
import logging
import argparse
import requests
from rich.tree import Tree
from datetime import datetime
from rich import print as xprint
from rich.markdown import Markdown
from rich.logging import RichHandler
start_time = datetime.now()
logging.basicConfig(level="NOTSET", format="%(message)s", handlers=[RichHandler(markup=True, log_time_format='[%H:%M:%S%p]')])
log = logging.getLogger("rich")
# Check if the remote tag_name from the latest release matches the one in the program
# if it does, it means the program is up-to-date.
# If it doesn't match, notify the user about a new release
def check_updates(version_tag):
response = requests.get("https://api.github.com/repos/bellingcat/reddit-post-scraping-tool/releases/latest").json()
if response['tag_name'] == version_tag:
pass
else:
raw_release_notes = response['body']
markdown_release_notes = Markdown(raw_release_notes)
log.info(f"A new release of reddit-post-scraping-tool is available ({response['tag_name']}). Run 'pip install --upgrade reddit-post-scraping-tool' to get the updates.")
xprint(markdown_release_notes)
# Getting posts
def get_posts(post):
post_data = {'Author': post['data']['author'],
'ID': post['data']['id'],
'Subreddit': post["data"]["subreddit_name_prefixed"],
'Visibility': post['data']['subreddit_type'],
# 'Author': post["data"]["author_fullname"],
'Thumbnail': post["data"]["thumbnail"],
# 'Flair': post["data"]["link_flair_text"],
'NSFW': post['data']['over_18'],
'Gilded': post['data']['gilded'],
'Upvotes': post["data"]["ups"],
'Upvote ratio': post["data"]["upvote_ratio"],
'Downvotes': post["data"]["downs"],
'Awards': post["data"]["total_awards_received"],
'Top award': post['data']['top_awarded_type'],
'Is crosspostable?': post['data']['is_crosspostable'],
'Score': post["data"]["score"],
'Category': post['data']['category'],
'Domain': post["data"]["domain"],
'Created': post['data']['created'],
'Approved at': post['data']['approved_at_utc'],
'Approved by': post['data']['approved_by'], }
post_tree = Tree("\n" + post['data']['title'])
for post_key, post_value in post_data.items():
post_tree.add(f"{post_key}: {post_value}")
xprint(post_tree)
print(post['data']['selftext'] + "\n")
def reddit_post_scraper():
session = requests.session()
session.headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15'}
response = session.get(f'https://reddit.com/r/{args.subreddit}/{args.listing}.json?limit={args.limit}&t={args.timeframe}').json()
found_posts = 0
for post in response['data']['children']:
if args.keyword.lower() in post['data']['selftext'] or args.keyword.lower() in post['data']['title']:
found_posts += 1
get_posts(post)
log.info(f"Keyword ('{args.keyword}') was found in {found_posts}/{len(response['data']['children'])} {args.listing} posts from r/{args.subreddit}.")
def create_parser():
parser = argparse.ArgumentParser(
description=f'reddit-post-scraping-tool — by Richard Mwewa | https://about.me/rly0nheart',
epilog=f'Given a subreddit name and a keyword, this program returns all top (by default) posts that contain the specified word. ')
parser.add_argument('-k', '--keyword', help='kewyword', required=True)
parser.add_argument('-s', '--subreddit', help='subreddit', required=True)
parser.add_argument('-c', '--limit', help='results limit (1-100) (default: %(default)s)', default=10, type=int)
parser.add_argument('-l', '--listing', default='top', const='top', nargs='?',
choices=['controversial', 'hot', 'best', 'new', 'rising'],
help='listings: controversial, hot, best, new, rising (default: %(default)s)')
parser.add_argument('-t', '--timeframe', default='all', const='all', nargs='?',
choices=['hour', 'day', 'week', 'month', 'year'],
help='timeframe: hour, day, week, month, year (default: %(default)s)')
return parser
_parser = create_parser()
args = _parser.parse_args()

29
rpst/__main.py Normal file
View File

@@ -0,0 +1,29 @@
from datetime import datetime
from rpst.__rpst import log, get_posts, check_updates, create_parser
def run():
"""
Main entry point for the program. It creates a parser, parses the command line arguments,
checks for updates, gets posts, and handles any exceptions that occur during the execution.
"""
# Create a parser and parse the command line arguments
parser = create_parser()
arguments = parser.parse_args()
# Record the start time
start_time = datetime.now()
try:
# Check for updates
check_updates(version_tag="1.4.0.0")
# Get posts with the provided/parsed arguments
get_posts(arguments=arguments)
except KeyboardInterrupt:
log.warning("User interruption detected.")
except Exception as e:
log.error(f"An error occurred: {e}")
finally:
log.info(f'Finished in {datetime.now() - start_time} seconds.')

201
rpst/__rpst_.py Normal file
View File

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