From 77711965c08b76971880a343b68aa545906cac4d Mon Sep 17 00:00:00 2001 From: Andy <88590076+AAndyProgram@users.noreply.github.com> Date: Sat, 30 Sep 2023 09:16:57 +0300 Subject: [PATCH] 2023.9.30.0 Add Threads.net API.UserDataBase: add 'EraseData_AdditionalDataFiles' function; add 'ThrowAnyImpl' function; add 'e' arg to 'LogError' function API.Instagram: make classes compatible with threads.net; add top limits; update container parsers; add an override to the 'EraseData_AdditionalDataFiles' function --- SCrawler/API/Base/UserDataBase.vb | 11 +- SCrawler/API/Instagram/Declarations.vb | 2 +- SCrawler/API/Instagram/SiteSettings.vb | 16 +- SCrawler/API/Instagram/UserData.vb | 117 +++++-- SCrawler/API/ThreadsNet/SiteSettings.vb | 166 ++++++++++ SCrawler/API/ThreadsNet/UserData.vb | 290 ++++++++++++++++++ .../Icons/SiteIcons/ThreadsIcon_192.ico | Bin 0 -> 169662 bytes .../PluginsEnvironment/Hosts/PluginHost.vb | 1 + SCrawler/SCrawler.vbproj | 5 + SCrawler/SiteResources.Designer.vb | 10 + SCrawler/SiteResources.resx | 3 + 11 files changed, 580 insertions(+), 41 deletions(-) create mode 100644 SCrawler/API/ThreadsNet/SiteSettings.vb create mode 100644 SCrawler/API/ThreadsNet/UserData.vb create mode 100644 SCrawler/Content/Icons/SiteIcons/ThreadsIcon_192.ico diff --git a/SCrawler/API/Base/UserDataBase.vb b/SCrawler/API/Base/UserDataBase.vb index a775605..445ae86 100644 --- a/SCrawler/API/Base/UserDataBase.vb +++ b/SCrawler/API/Base/UserDataBase.vb @@ -1829,6 +1829,7 @@ BlockNullPicture: If m.Contains(IUserData.EraseMode.History) Then If MyFilePosts.Delete(SFO.File, SFODelete.DeleteToRecycleBin, e) Then result = True If MyFileData.Delete(SFO.File, SFODelete.DeleteToRecycleBin, e) Then result = True + EraseData_AdditionalDataFiles() End If If m.Contains(IUserData.EraseMode.Data) Then Dim files As List(Of SFile) = SFile.GetFiles(DownloadContentDefault_GetRootDir.CSFileP,, SearchOption.AllDirectories, e) @@ -1850,6 +1851,8 @@ BlockNullPicture: Return ErrorsDescriber.Execute(EDP.SendToLog + EDP.ReturnValue, ex, $"EraseData({CInt(Mode)}): {ToStringForLog()}", False) End Try End Function + Protected Overridable Sub EraseData_AdditionalDataFiles() + End Sub Friend Overridable Function Delete(Optional ByVal Multiple As Boolean = False, Optional ByVal CollectionValue As Integer = -1) As Integer Implements IUserData.Delete Dim f As SFile = SFile.GetPath(MyFile.CutPath.Path) If f.Exists(SFO.Path, False) AndAlso (User.Merged OrElse f.Delete(SFO.Path, Settings.DeleteMode)) Then @@ -2026,8 +2029,8 @@ BlockNullPicture: End Function #End Region #Region "Errors functions" - Protected Sub LogError(ByVal ex As Exception, ByVal Message As String) - ErrorsDescriber.Execute(EDP.SendToLog, ex, $"{ToStringForLog()}: {Message}") + Protected Sub LogError(ByVal ex As Exception, ByVal Message As String, Optional ByVal e As ErrorsDescriber = Nothing) + ErrorsDescriber.Execute(If(e.Exists, e, New ErrorsDescriber(EDP.SendToLog)), ex, $"{ToStringForLog()}: {Message}") End Sub Protected Sub ErrorDownloading(ByVal f As SFile, ByVal URL As String) If Not f.Exists Then MyMainLOG = $"Error downloading from [{URL}] to [{f}]" @@ -2040,9 +2043,13 @@ BlockNullPicture: Private Overloads Sub ThrowAny() Implements IThrower.ThrowAny ThrowAny(TokenQueue) End Sub + ''' ThrowAnyImpl(Token) ''' ''' Friend Overridable Overloads Sub ThrowAny(ByVal Token As CancellationToken) + ThrowAnyImpl(Token) + End Sub + Protected Sub ThrowAnyImpl(ByVal Token As CancellationToken) Token.ThrowIfCancellationRequested() TokenQueue.ThrowIfCancellationRequested() TokenPersonal.ThrowIfCancellationRequested() diff --git a/SCrawler/API/Instagram/Declarations.vb b/SCrawler/API/Instagram/Declarations.vb index e8bed4c..ae1e125 100644 --- a/SCrawler/API/Instagram/Declarations.vb +++ b/SCrawler/API/Instagram/Declarations.vb @@ -17,7 +17,7 @@ Namespace API.Instagram Friend ReadOnly FilesPattern As RParams = RParams.DMS(".+?([^/\?]+?\.[\w\d]{3,4})(?=(\?|\Z))", 1, EDP.ReturnValue) Friend Sub UpdateResponser(ByVal Source As IResponse, ByRef Destination As Responser) Const r_wwwClaimName$ = "x-ig-set-www-claim" - Const r_tokenName$ = "csrftoken" + Const r_tokenName$ = SiteSettings.Header_CSRF_TOKEN_COOKIE If Not Source Is Nothing Then Dim isInternal As Boolean = TypeOf Source Is WebDataResponse Dim wwwClaimName$, tokenName$ diff --git a/SCrawler/API/Instagram/SiteSettings.vb b/SCrawler/API/Instagram/SiteSettings.vb index d73a00a..888d8ab 100644 --- a/SCrawler/API/Instagram/SiteSettings.vb +++ b/SCrawler/API/Instagram/SiteSettings.vb @@ -70,13 +70,14 @@ Namespace API.Instagram End Class #End Region #Region "Authorization properties" - Private Const Header_IG_APP_ID As String = "x-ig-app-id" + Friend Const Header_IG_APP_ID As String = "x-ig-app-id" Friend Const Header_IG_WWW_CLAIM As String = "x-ig-www-claim" Friend Const Header_CSRF_TOKEN As String = "x-csrftoken" - Private Const Header_ASBD_ID As String = "X-Asbd-Id" - Private Const Header_Browser As String = "Sec-Ch-Ua" - Private Const Header_BrowserExt As String = "Sec-Ch-Ua-Full-Version-List" - Private Const Header_Platform As String = "Sec-Ch-Ua-Platform-Version" + Friend Const Header_CSRF_TOKEN_COOKIE As String = "csrftoken" + Friend Const Header_ASBD_ID As String = "X-Asbd-Id" + Friend Const Header_Browser As String = "Sec-Ch-Ua" + Friend Const Header_BrowserExt As String = "Sec-Ch-Ua-Full-Version-List" + Friend Const Header_Platform As String = "Sec-Ch-Ua-Platform-Version" Friend ReadOnly Property HashTagged As PropertyValue @@ -365,13 +366,16 @@ Namespace API.Instagram SkipUntilNextSession = False End Sub #End Region -#Region "UserOptions, GetUserPostUrl" +#Region "UserOptions, GetUserUrl, GetUserPostUrl" Friend Overrides Sub UserOptions(ByRef Options As Object, ByVal OpenForm As Boolean) If Options Is Nothing OrElse Not TypeOf Options Is EditorExchangeOptions Then Options = New EditorExchangeOptions(Me) If OpenForm Then Using f As New InternalSettingsForm(Options, Me, False) : f.ShowDialog() : End Using End If End Sub + Friend Overrides Function GetUserUrl(ByVal User As IPluginContentProvider) As String + Return String.Format(UrlPatternUser, DirectCast(User, UserData).NameTrue) + End Function Friend Overrides Function GetUserPostUrl(ByVal User As UserDataBase, ByVal Media As UserMedia) As String Try Dim code$ = DirectCast(User, UserData).GetPostCodeById(Media.Post.ID) diff --git a/SCrawler/API/Instagram/UserData.vb b/SCrawler/API/Instagram/UserData.vb index 47026d0..e46f08a 100644 --- a/SCrawler/API/Instagram/UserData.vb +++ b/SCrawler/API/Instagram/UserData.vb @@ -30,7 +30,7 @@ Namespace API.Instagram Private Const Name_NameTrue As String = "NameTrue" #End Region #Region "Declarations" - Private Structure PostKV : Implements IEContainerProvider + Protected Structure PostKV : Implements IEContainerProvider Private Const Name_Code As String = "Code" Private Const Name_Section As String = "Section" Friend Code As String @@ -78,8 +78,8 @@ Namespace API.Instagram Friend Property GetStories As Boolean Friend Property GetStoriesUser As Boolean Friend Property GetTaggedData As Boolean - Private _NameTrue As String = String.Empty - Private ReadOnly Property NameTrue As String + Protected _NameTrue As String = String.Empty + Friend ReadOnly Property NameTrue As String Get Return _NameTrue.IfNullOrEmpty(Name) End Get @@ -143,12 +143,22 @@ Namespace API.Instagram Throw New ExitException End Sub End Class - Private Sub LoadSavePostsKV(ByVal Load As Boolean) + Private ReadOnly Property MyFilePostsKV As SFile + Get + Dim f As SFile = MyFilePosts + If Not f.IsEmptyString Then + f.Name &= "_KV" + f.Extension = "xml" + Return f + Else + Return Nothing + End If + End Get + End Property + Protected Sub LoadSavePostsKV(ByVal Load As Boolean) Dim x As XmlFile - Dim f As SFile = MyFilePosts + Dim f As SFile = MyFilePostsKV If Not f.IsEmptyString Then - f.Name &= "_KV" - f.Extension = "xml" If Load Then PostsKVIDs.Clear() x = New XmlFile(f, Protector.Modes.All, False) With {.AllowSameNames = True, .XmlReadOnly = True} @@ -182,10 +192,8 @@ Namespace API.Instagram Friend Function GetPostCodeById(ByVal PostID As String) As String Try If Not PostID.IsEmptyString Then - Dim f As SFile = MyFilePosts + Dim f As SFile = MyFilePostsKV If Not f.IsEmptyString Then - f.Name &= "_KV" - f.Extension = "xml" Dim l As List(Of PostKV) = Nothing Using x As New XmlFile(f, Protector.Modes.All, False) With {.AllowSameNames = True, .XmlReadOnly = True} x.LoadData() @@ -213,11 +221,15 @@ Namespace API.Instagram End If End Function Private _DownloadingInProgress As Boolean = False + Private _Limit As Integer = -1 + Private _TotalPostsParsed As Integer = 0 Protected Overrides Sub DownloadDataF(ByVal Token As CancellationToken) UserNameRequested = False Dim s As Sections = Sections.Timeline Dim errorFound As Boolean = False Try + _Limit = If(DownloadTopCount, -1) + _TotalPostsParsed = 0 LoadSavePostsKV(True) _DownloadingInProgress = True AddHandler Responser.ResponseReceived, AddressOf Responser_ResponseReceived @@ -270,7 +282,7 @@ Namespace API.Instagram Catch ex As Exception End Try End Sub - Private Sub UpdateResponser() + Protected Overridable Sub UpdateResponser() Try If _DownloadingInProgress AndAlso Not Responser Is Nothing AndAlso Not Responser.Disposed Then _DownloadingInProgress = False @@ -280,10 +292,10 @@ Namespace API.Instagram Catch End Try End Sub - Private Sub Responser_ResponseReceived(ByVal Sender As Object, ByVal e As EventArguments.WebDataResponse) + Protected Overridable Sub Responser_ResponseReceived(ByVal Sender As Object, ByVal e As EventArguments.WebDataResponse) Declarations.UpdateResponser(e, Responser) End Sub - Private Enum Sections : Timeline : Tagged : Stories : UserStories : SavedPosts : End Enum + Protected Enum Sections : Timeline : Tagged : Stories : UserStories : SavedPosts : End Enum Private Const StoriesFolder As String = "Stories" Private Const TaggedFolder As String = "Tagged" #Region "429 bypass" @@ -572,6 +584,7 @@ Namespace API.Instagram Dim URL$ = String.Empty Dim dValue% = 1 Dim _Index% = 0 + Dim before% If PostsToReparse.Count > 0 Then ProgressPre.ChangeMax(PostsToReparse.Count) Try Do While dValue = 1 @@ -600,7 +613,12 @@ Namespace API.Instagram If Not j Is Nothing Then If If(j("items")?.Count, 0) > 0 Then With j("items") - For Each jj In .Self : ObtainMedia(jj, PostsToReparse(i).ID) : Next + For Each jj In .Self + before = _TempMediaList.Count + ObtainMedia(jj, PostsToReparse(i).ID) + If Not before = _TempMediaList.Count Then _TotalPostsParsed += 1 + If _Limit > 0 And _TotalPostsParsed >= _Limit Then Throw New ExitException + Next End With End If j.Dispose() @@ -643,13 +661,17 @@ Namespace API.Instagram End Using End If End Sub - Private Function DefaultParser(ByVal Items As IEnumerable(Of EContainer), ByVal Section As Sections, ByVal Token As CancellationToken, - Optional ByVal SpecFolder As String = Nothing) As Boolean + Protected DefaultParser_ElemNode() As Object = Nothing + Protected DefaultParser_IgnorePass As Boolean = False + Protected DefaultParser_PostUrlCreator As Func(Of PostKV, String) = Function(post) $"https://www.instagram.com/p/{post.Code}/" + Protected Function DefaultParser(ByVal Items As IEnumerable(Of EContainer), ByVal Section As Sections, ByVal Token As CancellationToken, + Optional ByVal SpecFolder As String = Nothing) As Boolean ThrowAny(Token) If Items.Count > 0 Then Dim PostIDKV As PostKV Dim Pinned As Boolean - Dim PostDate$ + Dim PostDate$, PostOriginUrl$ + Dim before% If SpecFolder.IsEmptyString Then Select Case Section Case Sections.Tagged : SpecFolder = TaggedFolder @@ -660,22 +682,26 @@ Namespace API.Instagram ProgressPre.ChangeMax(Items.Count) For Each nn In Items ProgressPre.Perform() - With nn + With If(Not DefaultParser_ElemNode Is Nothing, nn.ItemF(DefaultParser_ElemNode), nn) PostIDKV = New PostKV(.Value("code"), .Value("id"), Section) + PostOriginUrl = DefaultParser_PostUrlCreator(PostIDKV) Pinned = .Contains("timeline_pinned_user_ids") - If PostKvExists(PostIDKV) Then + If Not DefaultParser_IgnorePass AndAlso PostKvExists(PostIDKV) Then If Not Pinned Then Return False Else _TempPostsList.Add(PostIDKV.ID) PostsKVIDs.ListAddValue(PostIDKV, LNC) PostDate = .Value("taken_at") - If Not IsSavedPosts Then + If Not DefaultParser_IgnorePass And Not IsSavedPosts Then Select Case CheckDatesLimit(PostDate, UnixDate32Provider) Case DateResult.Skip : Continue For Case DateResult.Exit : If Not Pinned Then Return False End Select End If - ObtainMedia(.Self, PostIDKV.ID, SpecFolder, PostDate) + before = _TempMediaList.Count + ObtainMedia(.Self, PostIDKV.ID, SpecFolder, PostDate,, PostOriginUrl) + If Not before = _TempMediaList.Count Then _TotalPostsParsed += 1 + If _Limit > 0 And _TotalPostsParsed >= _Limit Then Return False End If End With Next @@ -686,7 +712,7 @@ Namespace API.Instagram End Function #End Region #Region "Code ID converters" - Private Function CodeToID(ByVal Code As String) As String + Protected Function CodeToID(ByVal Code As String) As String Const CodeSymbols$ = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" Try If Not Code.IsEmptyString Then @@ -706,12 +732,19 @@ Namespace API.Instagram End Function #End Region #Region "Obtain Media" - Private Sub ObtainMedia(ByVal n As EContainer, ByVal PostID As String, Optional ByVal SpecialFolder As String = Nothing, - Optional ByVal DateObj As String = Nothing) + Protected ObtainMedia_SizeFuncVid As Func(Of EContainer, Sizes) = Nothing + Protected ObtainMedia_SizeFuncPic As Func(Of EContainer, Sizes) = Nothing + Protected ObtainMedia_AllowAbstract As Boolean = False + Protected Sub ObtainMedia(ByVal n As EContainer, ByVal PostID As String, Optional ByVal SpecialFolder As String = Nothing, + Optional ByVal DateObj As String = Nothing, Optional ByVal InitialType As Integer = -1, + Optional ByVal PostOriginUrl As String = Nothing) Try + Dim wrongData As Predicate(Of Sizes) = Function(_ss) _ss.HasError Or _ss.Data.IsEmptyString Dim img As Predicate(Of EContainer) = Function(_img) Not _img.Name.IsEmptyString AndAlso _img.Name.StartsWith("image_versions") AndAlso _img.Count > 0 Dim vid As Predicate(Of EContainer) = Function(_vid) Not _vid.Name.IsEmptyString AndAlso _vid.Name.StartsWith("video_versions") AndAlso _vid.Count > 0 Dim ss As Func(Of EContainer, Sizes) = Function(_ss) New Sizes(_ss.Value("width"), _ss.Value("url")) + Dim ssVid As Func(Of EContainer, Sizes) = ss + Dim ssPic As Func(Of EContainer, Sizes) = ss Dim mDate As Func(Of EContainer, String) = Function(ByVal elem As EContainer) As String If Not DateObj.IsEmptyString Then Return DateObj If elem.Contains("taken_at") Then @@ -731,28 +764,41 @@ Namespace API.Instagram End If End If End Function + If Not ObtainMedia_SizeFuncVid Is Nothing Then ssVid = ObtainMedia_SizeFuncVid + If Not ObtainMedia_SizeFuncPic Is Nothing Then ssPic = ObtainMedia_SizeFuncPic If n.Count > 0 Then Dim l As New List(Of Sizes) Dim d As EContainer Dim t% + Dim abstractDecision As Boolean = False '8 - gallery '2 - one video '1 - one picture t = n.Value("media_type").FromXML(Of Integer)(-1) + If t = -1 And InitialType = 8 And ObtainMedia_AllowAbstract Then + If n.Contains(vid) Then + t = 2 + abstractDecision = True + ElseIf n.Contains(img) Then + t = 1 + abstractDecision = True + End If + End If If t >= 0 Then Select Case t Case 1 If n.Contains(img) Then - t = n.Value("media_type").FromXML(Of Integer)(-1) + If Not abstractDecision Then t = n.Value("media_type").FromXML(Of Integer)(-1) DateObj = mDate(n) If t >= 0 Then With n.ItemF({img, "candidates"}).XmlIfNothing If .Count > 0 Then l.Clear() - l.ListAddList(.Select(ss), LNC) + l.ListAddList(.Select(ssPic), LNC) + If l.Count > 0 Then l.RemoveAll(wrongData) If l.Count > 0 Then l.Sort() - _TempMediaList.ListAddValue(MediaFromData(UTypes.Picture, l.First.Data, PostID, DateObj, SpecialFolder), LNC) + _TempMediaList.ListAddValue(MediaFromData(UTypes.Picture, l.First.Data, PostID, DateObj, SpecialFolder, PostOriginUrl), LNC) l.Clear() End If End If @@ -765,10 +811,11 @@ Namespace API.Instagram With n.ItemF({vid}).XmlIfNothing If .Count > 0 Then l.Clear() - l.ListAddList(.Select(ss), LNC) + l.ListAddList(.Select(ssVid), LNC) + If l.Count > 0 Then l.RemoveAll(wrongData) If l.Count > 0 Then l.Sort() - _TempMediaList.ListAddValue(MediaFromData(UTypes.Video, l.First.Data, PostID, DateObj, SpecialFolder), LNC) + _TempMediaList.ListAddValue(MediaFromData(UTypes.Video, l.First.Data, PostID, DateObj, SpecialFolder, PostOriginUrl), LNC) l.Clear() End If End If @@ -778,7 +825,7 @@ Namespace API.Instagram DateObj = mDate(n) With n("carousel_media").XmlIfNothing If .Count > 0 Then - For Each d In .Self : ObtainMedia(d, PostID, SpecialFolder, DateObj) : Next + For Each d In .Self : ObtainMedia(d, PostID, SpecialFolder, DateObj, 8, PostOriginUrl) : Next End If End With End Select @@ -939,6 +986,12 @@ Namespace API.Instagram DownloadContentDefault(Token) End Sub #End Region +#Region "Erase" + Protected Overrides Sub EraseData_AdditionalDataFiles() + Dim f As SFile = MyFilePostsKV + If f.Exists Then f.Delete(SFO.File, SFODelete.DeleteToRecycleBin, EDP.ReturnValue) + End Sub +#End Region #Region "Exceptions" ''' ''' @@ -996,9 +1049,9 @@ Namespace API.Instagram #End Region #Region "Create media" Private Function MediaFromData(ByVal t As UTypes, ByVal _URL As String, ByVal PostID As String, ByVal PostDate As String, - Optional ByVal SpecialFolder As String = Nothing) As UserMedia + Optional ByVal SpecialFolder As String = Nothing, Optional ByVal PostOriginUrl As String = Nothing) As UserMedia _URL = LinkFormatterSecure(RegexReplace(_URL.Replace("\", String.Empty), LinkPattern)) - Dim m As New UserMedia(_URL, t) With {.Post = New UserPost With {.ID = PostID}} + Dim m As New UserMedia(_URL, t) With {.URL_BASE = PostOriginUrl.IfNullOrEmpty(_URL), .Post = New UserPost With {.ID = PostID}} If Not m.URL.IsEmptyString Then m.File = CStr(RegexReplace(m.URL, FilesPattern)) If Not PostDate.IsEmptyString Then m.Post.Date = AConvert(Of Date)(PostDate, UnixDate32Provider, Nothing) Else m.Post.Date = Nothing m.SpecialFolder = SpecialFolder diff --git a/SCrawler/API/ThreadsNet/SiteSettings.vb b/SCrawler/API/ThreadsNet/SiteSettings.vb new file mode 100644 index 0000000..cd04063 --- /dev/null +++ b/SCrawler/API/ThreadsNet/SiteSettings.vb @@ -0,0 +1,166 @@ +' Copyright (C) 2023 Andy https://github.com/AAndyProgram +' This program is free software: you can redistribute it and/or modify +' it under the terms of the GNU General Public License as published by +' the Free Software Foundation, either version 3 of the License, or +' (at your option) any later version. +' +' This program is distributed in the hope that it will be useful, +' but WITHOUT ANY WARRANTY +Imports SCrawler.API.Base +Imports SCrawler.Plugin +Imports SCrawler.Plugin.Attributes +Imports PersonalUtilities.Tools.Web.Clients +Imports PersonalUtilities.Tools.Web.Cookies +Imports PersonalUtilities.Functions.RegularExpressions +Imports IG = SCrawler.API.Instagram.SiteSettings +Namespace API.ThreadsNet + + Friend Class SiteSettings : Inherits SiteSettingsBase +#Region "Declarations" + Friend Overrides ReadOnly Property Icon As Icon + Get + Return My.Resources.SiteResources.ThreadsIcon_192 + End Get + End Property + Private ReadOnly _Image As Image + Friend Overrides ReadOnly Property Image As Image + Get + Return _Image + End Get + End Property +#Region "Authorization" + + Friend ReadOnly Property HH_CSRF_TOKEN As PropertyValue + + Friend Property HH_IG_APP_ID As PropertyValue + + Friend Property HH_ASBD_ID As PropertyValue + + Private Property HH_BROWSER As PropertyValue + + Private Property HH_BROWSER_EXT As PropertyValue + + Private Property HH_PLATFORM As PropertyValue + + Private ReadOnly Property HH_USER_AGENT As PropertyValue + Private Sub ChangeResponserFields(ByVal PropName As String, ByVal Value As Object) + If Not PropName.IsEmptyString Then + Dim f$ = String.Empty + Dim isUserAgent As Boolean = False + Select Case PropName + Case NameOf(HH_IG_APP_ID) : f = IG.Header_IG_APP_ID + Case NameOf(HH_ASBD_ID) : f = IG.Header_ASBD_ID + Case NameOf(HH_CSRF_TOKEN) : f = IG.Header_CSRF_TOKEN + Case NameOf(HH_BROWSER) : f = IG.Header_Browser + Case NameOf(HH_BROWSER_EXT) : f = IG.Header_BrowserExt + Case NameOf(HH_PLATFORM) : f = IG.Header_Platform + Case NameOf(HH_USER_AGENT) : isUserAgent = True + End Select + If Not f.IsEmptyString Then + Responser.Headers.Remove(f) + If Not CStr(Value).IsEmptyString Then Responser.Headers.Add(f, CStr(Value)) + ElseIf isUserAgent Then + Responser.UserAgent = CStr(Value) + End If + End If + End Sub +#End Region +#End Region +#Region "Initializer" + Friend Sub New() + MyBase.New("Threads", "threads.net") + _AllowUserAgentUpdate = False + _Image = My.Resources.SiteResources.ThreadsIcon_192.ToBitmap + + Dim app_id$ = String.Empty + Dim token$ = String.Empty + Dim asbd$ = String.Empty + Dim browser$ = String.Empty + Dim browserExt$ = String.Empty + Dim platform$ = String.Empty + Dim useragent$ = String.Empty + + With Responser + .Accept = "*/*" + 'URGENT: remove after debug + .DeclaredError = EDP.SendToLog + EDP.ThrowException + If .UserAgentExists Then useragent = .UserAgent + With .Headers + If .Count > 0 Then + token = .Value(IG.Header_CSRF_TOKEN) + app_id = .Value(IG.Header_IG_APP_ID) + asbd = .Value(IG.Header_ASBD_ID) + browser = .Value(IG.Header_Browser) + browserExt = .Value(IG.Header_BrowserExt) + platform = .Value(IG.Header_Platform) + End If + .Add("Authority", "www.threads.net") + .Add("Origin", "https://www.threads.net") + .Add("Upgrade-Insecure-Requests", 1) + .Add("Sec-Ch-Ua-Model", "") + .Add("Sec-Ch-Ua-Mobile", "?0") + .Add("Sec-Ch-Ua-Platform", """Windows""") + .Add("Sec-Fetch-Dest", "empty") + .Add("Sec-Fetch-Mode", "cors") + .Add("Sec-Fetch-Site", "same-origin") + .Add("Sec-Fetch-User", "?1") + .Add("x-fb-friendly-name", "BarcelonaProfileThreadsTabRefetchableQuery") + End With + .CookiesExtractMode = Responser.CookiesExtractModes.Any + .CookiesUpdateMode = CookieKeeper.UpdateModes.ReplaceByNameAll + .CookiesExtractedAutoSave = False + .Cookies.ChangedAllowInternalDrop = False + .Cookies.Changed = False + End With + + HH_CSRF_TOKEN = New PropertyValue(token, GetType(String), Sub(v) ChangeResponserFields(NameOf(HH_CSRF_TOKEN), v)) + HH_IG_APP_ID = New PropertyValue(app_id, GetType(String), Sub(v) ChangeResponserFields(NameOf(HH_IG_APP_ID), v)) + HH_ASBD_ID = New PropertyValue(asbd, GetType(String), Sub(v) ChangeResponserFields(NameOf(HH_ASBD_ID), v)) + HH_BROWSER = New PropertyValue(browser, GetType(String), Sub(v) ChangeResponserFields(NameOf(HH_BROWSER), v)) + HH_BROWSER_EXT = New PropertyValue(browserExt, GetType(String), Sub(v) ChangeResponserFields(NameOf(HH_BROWSER_EXT), v)) + HH_PLATFORM = New PropertyValue(platform, GetType(String), Sub(v) ChangeResponserFields(NameOf(HH_PLATFORM), v)) + HH_USER_AGENT = New PropertyValue(useragent, GetType(String), Sub(v) ChangeResponserFields(NameOf(HH_USER_AGENT), v)) + + UrlPatternUser = "https://www.threads.net/@{0}" + UserRegex = RParams.DMS("threads.net/@([^/\?&]+)", 1) + ImageVideoContains = "threads.net" + End Sub +#End Region +#Region "UpdateResponserData" + Friend Sub UpdateResponserData(ByVal Resp As Responser) + With Responser.Cookies + Dim csrf$ = String.Empty + .Update(Resp.Cookies) + If .Changed Then + Responser.SaveCookies() + .Changed = False + csrf = If(.FirstOrDefault(Function(c) c.Name.StringToLower = IG.Header_CSRF_TOKEN_COOKIE)?.Value, String.Empty) + End If + If Not csrf.IsEmptyString AndAlso Not AEquals(Of String)(csrf, HH_CSRF_TOKEN.Value) Then HH_CSRF_TOKEN.Value = csrf + End With + End Sub +#End Region +#Region "GetInstance" + Friend Overrides Function GetInstance(ByVal What As ISiteSettings.Download) As IPluginContentProvider + Return New UserData + End Function +#End Region +#Region "BaseAuthExists, GetUserUrl, GetUserPostUrl" + Friend Overrides Function BaseAuthExists() As Boolean + Return Responser.CookiesExists And {HH_CSRF_TOKEN, HH_IG_APP_ID}.All(Function(v) ACheck(Of String)(v.Value)) + End Function + Friend Overrides Function GetUserUrl(ByVal User As IPluginContentProvider) As String + Return String.Format(UrlPatternUser, DirectCast(User, UserData).NameTrue) + End Function + Friend Overrides Function GetUserPostUrl(ByVal User As UserDataBase, ByVal Media As UserMedia) As String + Try + Dim code$ = DirectCast(User, UserData).GetPostCodeById(Media.Post.ID) + Dim name$ = DirectCast(User, UserData).NameTrue + If Not code.IsEmptyString Then Return $"https://www.threads.net/@{name}/post/{code}/" Else Return String.Empty + Catch ex As Exception + Return ErrorsDescriber.Execute(EDP.SendToLog, ex, "Can't open user's post", String.Empty) + End Try + End Function +#End Region + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler/API/ThreadsNet/UserData.vb b/SCrawler/API/ThreadsNet/UserData.vb new file mode 100644 index 0000000..8562aa6 --- /dev/null +++ b/SCrawler/API/ThreadsNet/UserData.vb @@ -0,0 +1,290 @@ +' Copyright (C) 2023 Andy https://github.com/AAndyProgram +' This program is free software: you can redistribute it and/or modify +' it under the terms of the GNU General Public License as published by +' the Free Software Foundation, either version 3 of the License, or +' (at your option) any later version. +' +' This program is distributed in the hope that it will be useful, +' but WITHOUT ANY WARRANTY +Imports System.Threading +Imports SCrawler.API.Base +Imports SCrawler.API.YouTube.Objects +Imports PersonalUtilities.Functions.XML +Imports PersonalUtilities.Functions.RegularExpressions +Imports PersonalUtilities.Tools.Web.Documents.JSON +Imports PersonalUtilities.Tools.Web.Clients +Imports PersonalUtilities.Tools.Web.Clients.EventArguments +Imports IGS = SCrawler.API.Instagram.SiteSettings +Namespace API.ThreadsNet + Friend Class UserData : Inherits Instagram.UserData +#Region "Declarations" + Private Const Header_FB_LSD As String = "x-fb-lsd" + Private ReadOnly Property MySettings As SiteSettings + Get + Return HOST.Source + End Get + End Property + Private ReadOnly ObtainMedia_SizeFuncPic_RegexP As RParams = RParams.DMS("_p(\d+)x(\d+)", 1, EDP.ReturnValue) + Private ReadOnly ObtainMedia_SizeFuncPic_RegexS As RParams = RParams.DMS("_s(\d+)x(\d+)", 1, EDP.ReturnValue) + Private ReadOnly DefaultParser_ElemNode_Default() As Object = {"node", "thread_items", 0, "post"} + Private OPT_LSD As String = String.Empty + Private OPT_FB_DTSG As String = String.Empty + Private ReadOnly Property Valid As Boolean + Get + Return Not OPT_LSD.IsEmptyString And Not OPT_FB_DTSG.IsEmptyString And Not ID.IsEmptyString + End Get + End Property +#End Region +#Region "Loader" + Protected Overrides Sub LoadUserInformation_OptionalFields(ByRef Container As XmlFile, ByVal Loading As Boolean) + End Sub +#End Region +#Region "Exchange" + Friend Overrides Function ExchangeOptionsGet() As Object + Return Nothing + End Function + Friend Overrides Sub ExchangeOptionsSet(ByVal Obj As Object) + End Sub +#End Region +#Region "Initializer" + Friend Sub New() + ObtainMedia_SizeFuncPic = Function(ByVal ss As EContainer) As Sizes + If ss.Value("url").IsEmptyString Then + Return New Sizes("----", "") + ElseIf Not ss.Value("width").IsEmptyString Then + Return New Sizes(ss.Value("height").IfNullOrEmpty(ss.Value("width")), ss.Value("url")) + Else + Dim rval$ = RegexReplace(ss.Value("url"), ObtainMedia_SizeFuncPic_RegexP) + If Not rval.IsEmptyString Then Return New Sizes(rval, ss.Value("url")) + rval = RegexReplace(ss.Value("url"), ObtainMedia_SizeFuncPic_RegexS) + If Not rval.IsEmptyString Then Return New Sizes(AConvert(Of Integer)(rval, 1) * -1, ss.Value("url")) + Return New Sizes(10000, ss.Value("url")) + End If + End Function + ObtainMedia_SizeFuncVid = Function(ss) If(ss.Value("url").IsEmptyString, New Sizes("----", ""), New Sizes(10000, ss.Value("url"))) + ObtainMedia_AllowAbstract = True + DefaultParser_ElemNode = DefaultParser_ElemNode_Default + DefaultParser_PostUrlCreator = Function(post) $"https://www.threads.net/@{NameTrue}/post/{post.Code}" + End Sub +#End Region +#Region "Download functions" + Protected Overrides Sub DownloadDataF(ByVal Token As CancellationToken) + Dim errorFound As Boolean = False + Try + Responser.Method = "POST" + AddHandler Responser.ResponseReceived, AddressOf Responser_ResponseReceived + LoadSavePostsKV(True) + OPT_LSD = String.Empty + OPT_FB_DTSG = String.Empty + DownloadData(String.Empty, Token) + Catch ex As Exception + errorFound = True + Throw ex + Finally + Responser.Method = "POST" + UpdateResponser() + MySettings.UpdateResponserData(Responser) + If Not errorFound Then LoadSavePostsKV(False) + End Try + End Sub + Protected Overrides Sub UpdateResponser() + If Not Responser Is Nothing AndAlso Not Responser.Disposed Then + RemoveHandler Responser.ResponseReceived, AddressOf Responser_ResponseReceived + End If + End Sub + Protected Overrides Sub Responser_ResponseReceived(ByVal Sender As Object, ByVal e As WebDataResponse) + If e.CookiesExists Then + Dim csrf$ = If(e.Cookies.FirstOrDefault(Function(v) v.Name.StringToLower = IGS.Header_CSRF_TOKEN_COOKIE)?.Value, String.Empty) + If Not csrf.IsEmptyString AndAlso Not AEquals(Of String)(csrf, Responser.Headers.Value(IGS.Header_CSRF_TOKEN)) Then _ + Responser.Headers.Add(IGS.Header_CSRF_TOKEN, csrf) + End If + End Sub + Private Overloads Sub DownloadData(ByVal Cursor As String, ByVal Token As CancellationToken) + Const urlPattern$ = "https://www.threads.net/api/graphql?lsd={0}&variables={1}&doc_id=6371597506283707&fb_api_req_friendly_name=BarcelonaProfileThreadsTabRefetchableQuery&server_timestamps=true&fb_dtsg={2}" + Const var_init$ = """userID"":""{0}""" + Const var_cursor$ = """after"":""{1}"",""before"":null,""first"":25,""last"":null,""userID"":""{0}"",""__relay_internal__pv__BarcelonaIsLoggedInrelayprovider"":true,""__relay_internal__pv__BarcelonaIsFeedbackHubEnabledrelayprovider"":false" + Dim URL$ = String.Empty + Try + If Not Valid Then + Dim idIsNull As Boolean = ID.IsEmptyString + UpdateCredentials() + If idIsNull And Not ID.IsEmptyString Then _ForceSaveUserInfo = True + End If + If Not Valid Then Throw New Plugin.ExitException("Some credentials are missing") + + Responser.Method = "POST" + Responser.Referer = $"https://www.threads.net/@{NameTrue}" + Responser.Headers.Add(Header_FB_LSD, OPT_LSD) + + Dim nextCursor$ = String.Empty + Dim dataFound As Boolean = False + + Dim vars$ + If Cursor.IsEmptyString Then + vars = String.Format(var_init, ID) + Else + vars = String.Format(var_cursor, ID, Cursor) + End If + vars = SymbolsConverter.ASCII.EncodeSymbolsOnly("{" & vars & "}") + + URL = String.Format(urlPattern, OPT_LSD, vars, SymbolsConverter.ASCII.EncodeSymbolsOnly(OPT_FB_DTSG)) + + Using j As EContainer = GetDocument(URL, Token) + If j.ListExists Then + With j({"data", "mediaData"}) + If .ListExists Then + nextCursor = .Value({"page_info"}, "end_cursor") + With .Item({"edges"}) + If .ListExists Then dataFound = DefaultParser(.Self, Sections.Timeline, Token) + End With + End If + End With + End If + End Using + + If dataFound And Not nextCursor.IsEmptyString Then DownloadData(nextCursor, Token) + Catch ex As Exception + ProcessException(ex, Token, $"data downloading error [{URL}]") + End Try + End Sub + Private Function GetDocument(ByVal URL As String, ByVal Token As CancellationToken, Optional ByVal Round As Integer = 0) As EContainer + Try + ThrowAny(Token) + If Round > 0 AndAlso Not UpdateCredentials() Then Throw New Exception("Failed to update credentials") + ThrowAny(Token) + Dim r$ = Responser.GetResponse(URL) + If Not r.IsEmptyString Then Return JsonDocument.Parse(r) Else Throw New Exception("Failed to get a response") + Catch ex As Exception + If Round = 0 Then + Return GetDocument(URL, Token, Round + 1) + Else + Throw ex + End If + End Try + End Function + Private Function UpdateCredentials(Optional ByVal e As ErrorsDescriber = Nothing) As Boolean + Dim URL$ = $"https://www.threads.net/@{NameTrue}" + OPT_LSD = String.Empty + OPT_FB_DTSG = String.Empty + Try + Responser.Method = "GET" + Responser.Referer = URL + Responser.Headers.Remove(Header_FB_LSD) + Dim r$ = Responser.GetResponse(URL,, EDP.SendToLog + EDP.ThrowException) + Dim rr As RParams + Dim tt$, ttVal$ + If Not r.IsEmptyString Then + rr = RParams.DM("\[\],{""token"":""(.*?)""},\d+\]", 0, RegexReturn.List, EDP.ReturnValue) + Dim tokens As List(Of String) = RegexReplace(r, rr) + If tokens.ListExists Then + With rr + .Match = Nothing + .MatchSub = 1 + .WhatGet = RegexReturn.Value + End With + For Each tt In tokens + If Not OPT_FB_DTSG.IsEmptyString And Not OPT_LSD.IsEmptyString Then + Exit For + Else + ttVal = RegexReplace(tt, rr) + If Not ttVal.IsEmptyString Then + If ttVal.Contains(":") Then + If OPT_FB_DTSG.IsEmptyString Then OPT_FB_DTSG = ttVal + Else + If OPT_LSD.IsEmptyString Then OPT_LSD = ttVal + End If + End If + End If + Next + End If + If ID.IsEmptyString Then ID = RegexReplace(r, RParams.DMS("""props"":\{""user_id"":""(\d+)""\},", 1, EDP.ReturnValue)) + End If + Return Valid + Catch ex As Exception + Dim notFound$ = String.Empty + If OPT_FB_DTSG.IsEmptyString Then notFound.StringAppend(Header_FB_LSD) + If OPT_LSD.IsEmptyString Then notFound.StringAppend("lsd") + If ID.IsEmptyString Then notFound.StringAppend("User ID") + LogError(ex, $"failed to update some{IIf(notFound.IsEmptyString, String.Empty, $" ({notFound})")} credentials", e) + Return False + End Try + End Function +#End Region +#Region "ReparseMissing" + Protected Overrides Sub ReparseMissing(ByVal Token As CancellationToken) + Const varsPattern$ = """postID"":""{0}"",""userID"":""{1}"",""__relay_internal__pv__BarcelonaIsLoggedInrelayprovider"":true,""__relay_internal__pv__BarcelonaIsFeedbackHubEnabledrelayprovider"":false" + 'Const varsPattern$ = "{""postID"":""{0}"",""__relay_internal__pv__BarcelonaIsLoggedInrelayprovider"":true,""__relay_internal__pv__BarcelonaIsFeedbackHubEnabledrelayprovider"":false}" + Const urlPattern$ = "https://www.threads.net/api/graphql?lsd={0}&variables={1}&fb_api_req_friendly_name=BarcelonaPostPageQuery&server_timestamps=true&fb_dtsg={2}&doc_id=25460088156920903" + Dim rList As New List(Of Integer) + Dim URL$ = String.Empty + DefaultParser_ElemNode = Nothing + DefaultParser_IgnorePass = True + Try + If ContentMissingExists Then + Responser.Method = "POST" + Responser.Referer = $"https://www.threads.net/@{NameTrue}" + If Not IsSingleObjectDownload AndAlso Not UpdateCredentials() Then Throw New Exception("Failed to update credentials") + Dim m As UserMedia + Dim vars$ + Dim j As EContainer + ProgressPre.ChangeMax(_ContentList.Count) + For i% = 0 To _ContentList.Count - 1 + ProgressPre.Perform() + m = _ContentList(i) + If m.State = UserMedia.States.Missing And Not m.Post.ID.IsEmptyString Then + ThrowAny(Token) + vars = SymbolsConverter.ASCII.EncodeSymbolsOnly("{" & String.Format(varsPattern, m.Post.ID.Split("_").FirstOrDefault, ID) & "}") + URL = String.Format(urlPattern, OPT_LSD, vars, SymbolsConverter.ASCII.EncodeSymbolsOnly(OPT_FB_DTSG)) + + j = GetDocument(URL, Token) + If j.ListExists Then + With j.ItemF({"data", "data", "edges", 0, "node", "thread_items", 0, "post"}) + If .ListExists AndAlso DefaultParser({ .Self}, Sections.Timeline, Token) Then rList.Add(i) + End With + j.Dispose() + End If + End If + Next + End If + Catch ex As Exception + ProcessException(ex, Token, $"ReparseMissing error [{URL}]") + Finally + DefaultParser_ElemNode = DefaultParser_ElemNode_Default + DefaultParser_IgnorePass = False + If rList.Count > 0 Then + For i% = rList.Count - 1 To 0 Step -1 : _ContentList.RemoveAt(rList(i)) : Next + rList.Clear() + End If + End Try + End Sub +#End Region +#Region "DownloadSingleObject" + Protected Overrides Sub DownloadSingleObject_GetPosts(ByVal Data As IYouTubeMediaContainer, ByVal Token As CancellationToken) + Dim url$ = Data.URL_BASE.IfNullOrEmpty(Data.URL) + Dim postCode$ = RegexReplace(url, RParams.DMS("post/([^/\?&]+)", 1, EDP.ReturnValue)) + If Not postCode.IsEmptyString Then + Dim postId$ = CodeToID(postCode) + If Not postId.IsEmptyString Then + _NameTrue = MySettings.IsMyUser(url).UserName + DefaultParser_PostUrlCreator = Function(post) url + If Not _NameTrue.IsEmptyString AndAlso UpdateCredentials(EDP.ReturnValue) Then + _ContentList.Add(New UserMedia(url) With {.State = UserMedia.States.Missing, .Post = postId}) + ReparseMissing(Token) + End If + End If + End If + End Sub +#End Region +#Region "ThrowAny" + Friend Overrides Sub ThrowAny(ByVal Token As CancellationToken) + ThrowAnyImpl(Token) + End Sub +#End Region +#Region "DownloadingException" + Protected Overrides Function DownloadingException(ByVal ex As Exception, ByVal Message As String, Optional ByVal FromPE As Boolean = False, + Optional ByVal EObj As Object = Nothing) As Integer + Return 0 + End Function +#End Region + End Class +End Namespace \ No newline at end of file diff --git a/SCrawler/Content/Icons/SiteIcons/ThreadsIcon_192.ico b/SCrawler/Content/Icons/SiteIcons/ThreadsIcon_192.ico new file mode 100644 index 0000000000000000000000000000000000000000..bc454b49c9025723a8cc9c33fe1e41050fa9a169 GIT binary patch literal 169662 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 + + @@ -709,6 +711,9 @@ + + + diff --git a/SCrawler/SiteResources.Designer.vb b/SCrawler/SiteResources.Designer.vb index 4f30e04..70442f3 100644 --- a/SCrawler/SiteResources.Designer.vb +++ b/SCrawler/SiteResources.Designer.vb @@ -264,6 +264,16 @@ Namespace My.Resources End Get End Property + ''' + ''' Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + ''' + Friend Shared ReadOnly Property ThreadsIcon_192() As System.Drawing.Icon + Get + Dim obj As Object = ResourceManager.GetObject("ThreadsIcon_192", resourceCulture) + Return CType(obj,System.Drawing.Icon) + End Get + End Property + ''' ''' Looks up a localized resource of type System.Drawing.Icon similar to (Icon). ''' diff --git a/SCrawler/SiteResources.resx b/SCrawler/SiteResources.resx index 73de61b..b86f2da 100644 --- a/SCrawler/SiteResources.resx +++ b/SCrawler/SiteResources.resx @@ -178,6 +178,9 @@ Content\Pictures\SitePictures\ThisVidPic_16.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + Content\Icons\SiteIcons\ThreadsIcon_192.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + Content\Icons\SiteIcons\TikTokIcon_32.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a