├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── TwitterSharp.sln ├── copy_coverage.sh ├── docs ├── .gitignore ├── api │ ├── .gitignore │ └── index.md ├── docfx.json ├── index.md └── toc.yml ├── src ├── ApiEndpoint │ ├── AccessLevel.cs │ ├── Endpoint.cs │ ├── EndpointAttribute.cs │ ├── EndpointType.cs │ └── Resource.cs ├── AssemblyAttributes.cs ├── Client │ ├── TwitterClient.cs │ └── TwitterException.cs ├── JsonOption │ ├── EntitiesConverter.cs │ ├── ExpressionConverter.cs │ ├── MediaConverter.cs │ ├── ReferencedTweetConverter.cs │ ├── ReplySettingsConverter.cs │ └── SnakeCaseNamingPolicy.cs ├── Model │ ├── IHaveAuthor.cs │ ├── IHaveMatchingRules.cs │ └── IHaveMedia.cs ├── Request │ ├── AdvancedSearch │ │ ├── MediaOption.cs │ │ ├── TweetOption.cs │ │ └── UserOption.cs │ ├── Internal │ │ ├── StreamRequestAdd.cs │ │ └── StreamRequestDelete.cs │ ├── Option │ │ ├── ASearchOptions.cs │ │ ├── TweetSearchOptions.cs │ │ └── UserSearchOptions.cs │ └── StreamRequest.cs ├── Response │ ├── Answer.cs │ ├── Entity │ │ ├── AEntity.cs │ │ ├── Entities.cs │ │ ├── EntityTag.cs │ │ └── EntityUrl.cs │ ├── Error.cs │ ├── RMedia │ │ ├── Media.cs │ │ ├── MediaType.cs │ │ └── MediaVariant.cs │ ├── RStream │ │ └── StreamInfo.cs │ ├── RTweet │ │ ├── Attachments.cs │ │ ├── EditControls.cs │ │ ├── MatchingRule.cs │ │ ├── ReferenceType.cs │ │ ├── ReferencedTweet.cs │ │ ├── ReplySettings.cs │ │ ├── Tweet.cs │ │ └── TweetPublicMetrics.cs │ ├── RUser │ │ ├── RUsers.cs │ │ ├── User.cs │ │ └── UserPublicMetrics.cs │ └── RateLimit.cs ├── Rule │ ├── Expression.cs │ ├── ExpressionType.cs │ └── RadiusUnit.cs └── TwitterSharp.csproj └── test ├── TestExpression.cs ├── TestFollow.cs ├── TestLike.cs ├── TestMedia.cs ├── TestRetweet.cs ├── TestStream.cs ├── TestTweet.cs ├── TestUser.cs └── TwitterSharp.UnitTests.csproj /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: .NET 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Setup .NET 17 | uses: actions/setup-dotnet@v3 18 | with: 19 | dotnet-version: | 20 | 5.x 21 | 6.x 22 | 7.x 23 | - name: Initialize CodeQL 24 | uses: github/codeql-action/init@v1 25 | - name: Restore dependencies 26 | run: dotnet restore 27 | - name: Build 28 | run: dotnet build --no-restore 29 | - name: Test 30 | env: 31 | TWITTER_TOKEN: ${{ secrets.TWITTER_TOKEN }} 32 | run: dotnet test --no-build --verbosity normal --collect:"XPlat Code Coverage" -p:ParallelizeTestCollections=false 33 | - name: Copy coverage 34 | run: sh copy_coverage.sh 35 | - name: Upload coverage to Codacy 36 | uses: codacy/codacy-coverage-reporter-action@master 37 | with: 38 | project-token: ${{ secrets.CODACY_TOKEN }} 39 | coverage-reports: coverage.xml 40 | - name: Perform CodeQL Analysis 41 | uses: github/codeql-action/analyze@v1 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | nuget.config 2 | 3 | ## Ignore Visual Studio temporary files, build results, and 4 | ## files generated by popular Visual Studio add-ons. 5 | ## 6 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 7 | 8 | # User-specific files 9 | *.rsuser 10 | *.suo 11 | *.user 12 | *.userosscache 13 | *.sln.docstates 14 | 15 | # User-specific files (MonoDevelop/Xamarin Studio) 16 | *.userprefs 17 | 18 | # Mono auto generated files 19 | mono_crash.* 20 | 21 | # Build results 22 | [Dd]ebug/ 23 | [Dd]ebugPublic/ 24 | [Rr]elease/ 25 | [Rr]eleases/ 26 | x64/ 27 | x86/ 28 | [Aa][Rr][Mm]/ 29 | [Aa][Rr][Mm]64/ 30 | bld/ 31 | [Bb]in/ 32 | [Oo]bj/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # StyleCop 67 | StyleCopReport.xml 68 | 69 | # Files built by Visual Studio 70 | *_i.c 71 | *_p.c 72 | *_h.h 73 | *.ilk 74 | *.meta 75 | *.obj 76 | *.iobj 77 | *.pch 78 | *.pdb 79 | *.ipdb 80 | *.pgc 81 | *.pgd 82 | *.rsp 83 | *.sbr 84 | *.tlb 85 | *.tli 86 | *.tlh 87 | *.tmp 88 | *.tmp_proj 89 | *_wpftmp.csproj 90 | *.log 91 | *.vspscc 92 | *.vssscc 93 | .builds 94 | *.pidb 95 | *.svclog 96 | *.scc 97 | 98 | # Chutzpah Test files 99 | _Chutzpah* 100 | 101 | # Visual C++ cache files 102 | ipch/ 103 | *.aps 104 | *.ncb 105 | *.opendb 106 | *.opensdf 107 | *.sdf 108 | *.cachefile 109 | *.VC.db 110 | *.VC.VC.opendb 111 | 112 | # Visual Studio profiler 113 | *.psess 114 | *.vsp 115 | *.vspx 116 | *.sap 117 | 118 | # Visual Studio Trace Files 119 | *.e2e 120 | 121 | # TFS 2012 Local Workspace 122 | $tf/ 123 | 124 | # Guidance Automation Toolkit 125 | *.gpState 126 | 127 | # ReSharper is a .NET coding add-in 128 | _ReSharper*/ 129 | *.[Rr]e[Ss]harper 130 | *.DotSettings.user 131 | 132 | # TeamCity is a build add-in 133 | _TeamCity* 134 | 135 | # DotCover is a Code Coverage Tool 136 | *.dotCover 137 | 138 | # AxoCover is a Code Coverage Tool 139 | .axoCover/* 140 | !.axoCover/settings.json 141 | 142 | # Visual Studio code coverage results 143 | *.coverage 144 | *.coveragexml 145 | 146 | # NCrunch 147 | _NCrunch_* 148 | .*crunch*.local.xml 149 | nCrunchTemp_* 150 | 151 | # MightyMoose 152 | *.mm.* 153 | AutoTest.Net/ 154 | 155 | # Web workbench (sass) 156 | .sass-cache/ 157 | 158 | # Installshield output folder 159 | [Ee]xpress/ 160 | 161 | # DocProject is a documentation generator add-in 162 | DocProject/buildhelp/ 163 | DocProject/Help/*.HxT 164 | DocProject/Help/*.HxC 165 | DocProject/Help/*.hhc 166 | DocProject/Help/*.hhk 167 | DocProject/Help/*.hhp 168 | DocProject/Help/Html2 169 | DocProject/Help/html 170 | 171 | # Click-Once directory 172 | publish/ 173 | 174 | # Publish Web Output 175 | *.[Pp]ublish.xml 176 | *.azurePubxml 177 | # Note: Comment the next line if you want to checkin your web deploy settings, 178 | # but database connection strings (with potential passwords) will be unencrypted 179 | *.pubxml 180 | *.publishproj 181 | 182 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 183 | # checkin your Azure Web App publish settings, but sensitive information contained 184 | # in these scripts will be unencrypted 185 | PublishScripts/ 186 | 187 | # NuGet Packages 188 | *.nupkg 189 | # NuGet Symbol Packages 190 | *.snupkg 191 | # The packages folder can be ignored because of Package Restore 192 | **/[Pp]ackages/* 193 | # except build/, which is used as an MSBuild target. 194 | !**/[Pp]ackages/build/ 195 | # Uncomment if necessary however generally it will be regenerated when needed 196 | #!**/[Pp]ackages/repositories.config 197 | # NuGet v3's project.json files produces more ignorable files 198 | *.nuget.props 199 | *.nuget.targets 200 | 201 | # Microsoft Azure Build Output 202 | csx/ 203 | *.build.csdef 204 | 205 | # Microsoft Azure Emulator 206 | ecf/ 207 | rcf/ 208 | 209 | # Windows Store app package directories and files 210 | AppPackages/ 211 | BundleArtifacts/ 212 | Package.StoreAssociation.xml 213 | _pkginfo.txt 214 | *.appx 215 | *.appxbundle 216 | *.appxupload 217 | 218 | # Visual Studio cache files 219 | # files ending in .cache can be ignored 220 | *.[Cc]ache 221 | # but keep track of directories ending in .cache 222 | !?*.[Cc]ache/ 223 | 224 | # Others 225 | ClientBin/ 226 | ~$* 227 | *~ 228 | *.dbmdl 229 | *.dbproj.schemaview 230 | *.jfm 231 | *.pfx 232 | *.publishsettings 233 | orleans.codegen.cs 234 | 235 | # Including strong name files can present a security risk 236 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 237 | #*.snk 238 | 239 | # Since there are multiple workflows, uncomment next line to ignore bower_components 240 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 241 | #bower_components/ 242 | 243 | # RIA/Silverlight projects 244 | Generated_Code/ 245 | 246 | # Backup & report files from converting an old project file 247 | # to a newer Visual Studio version. Backup files are not needed, 248 | # because we have git ;-) 249 | _UpgradeReport_Files/ 250 | Backup*/ 251 | UpgradeLog*.XML 252 | UpgradeLog*.htm 253 | ServiceFabricBackup/ 254 | *.rptproj.bak 255 | 256 | # SQL Server files 257 | *.mdf 258 | *.ldf 259 | *.ndf 260 | 261 | # Business Intelligence projects 262 | *.rdl.data 263 | *.bim.layout 264 | *.bim_*.settings 265 | *.rptproj.rsuser 266 | *- [Bb]ackup.rdl 267 | *- [Bb]ackup ([0-9]).rdl 268 | *- [Bb]ackup ([0-9][0-9]).rdl 269 | 270 | # Microsoft Fakes 271 | FakesAssemblies/ 272 | 273 | # GhostDoc plugin setting file 274 | *.GhostDoc.xml 275 | 276 | # Node.js Tools for Visual Studio 277 | .ntvs_analysis.dat 278 | node_modules/ 279 | 280 | # Visual Studio 6 build log 281 | *.plg 282 | 283 | # Visual Studio 6 workspace options file 284 | *.opt 285 | 286 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 287 | *.vbw 288 | 289 | # Visual Studio LightSwitch build output 290 | **/*.HTMLClient/GeneratedArtifacts 291 | **/*.DesktopClient/GeneratedArtifacts 292 | **/*.DesktopClient/ModelManifest.xml 293 | **/*.Server/GeneratedArtifacts 294 | **/*.Server/ModelManifest.xml 295 | _Pvt_Extensions 296 | 297 | # Paket dependency manager 298 | .paket/paket.exe 299 | paket-files/ 300 | 301 | # FAKE - F# Make 302 | .fake/ 303 | 304 | # CodeRush personal settings 305 | .cr/personal 306 | 307 | # Python Tools for Visual Studio (PTVS) 308 | __pycache__/ 309 | *.pyc 310 | 311 | # Cake - Uncomment if you are using it 312 | # tools/** 313 | # !tools/packages.config 314 | 315 | # Tabs Studio 316 | *.tss 317 | 318 | # Telerik's JustMock configuration file 319 | *.jmconfig 320 | 321 | # BizTalk build output 322 | *.btp.cs 323 | *.btm.cs 324 | *.odx.cs 325 | *.xsd.cs 326 | 327 | # OpenCover UI analysis results 328 | OpenCover/ 329 | 330 | # Azure Stream Analytics local run output 331 | ASALocalRun/ 332 | 333 | # MSBuild Binary and Structured Log 334 | *.binlog 335 | 336 | # NVidia Nsight GPU debugger configuration file 337 | *.nvuser 338 | 339 | # MFractors (Xamarin productivity tool) working folder 340 | .mfractor/ 341 | 342 | # Local History for Visual Studio 343 | .localhistory/ 344 | 345 | # BeatPulse healthcheck temp database 346 | healthchecksdb 347 | 348 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 349 | MigrationBackup/ 350 | 351 | # Ionide (cross platform F# VS Code tools) working folder 352 | .ionide/ 353 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Christian Chaux 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TwitterSharp 2 | C# wrapper around Twitter API V2 3 | 4 | | CI | Code Quality | Coverage | 5 | | -- | ------------ | -------- | 6 | | [![.NET](https://github.com/Xwilarg/TwitterSharp/actions/workflows/ci.yml/badge.svg)](https://github.com/Xwilarg/TwitterSharp/actions/workflows/ci.yml) | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/726fd5c6287644d48807fcf03a18d868)](https://www.codacy.com/gh/Xwilarg/TwitterSharp/dashboard?utm_source=github.com&utm_medium=referral&utm_content=Xwilarg/TwitterSharp&utm_campaign=Badge_Grade) | [![Codacy Badge](https://app.codacy.com/project/badge/Coverage/726fd5c6287644d48807fcf03a18d868)](https://www.codacy.com/gh/Xwilarg/TwitterSharp/dashboard?utm_source=github.com&utm_medium=referral&utm_content=Xwilarg/TwitterSharp&utm_campaign=Badge_Coverage) | 7 | 8 | ## Download 9 | 10 | The package is available on [NuGet](https://www.nuget.org/packages/TwitterSharp/) 11 | ```powershell 12 | Install-Package TwitterSharp 13 | ``` 14 | 15 | ## Documentation 16 | https://twittersharp.zirk.eu 17 | 18 | ## How does it works? 19 | 20 | To begin with, please go to the [Twitter Developer Portal](https://developer.twitter.com/) and create a new application\ 21 | Then you must instantiate a new client: 22 | ```cs 23 | var client = new TwitterSharp.Client.TwitterClient(bearerToken); 24 | ``` 25 | From there you can access various methods to access tweets and users, however please make note that a basic request only includes: 26 | - For tweets: its ID and content 27 | - For users: its ID, name and username 28 | 29 | To solve that, most function take an array of UserOption or TweetOption, make sure to add what you need there! 30 | 31 | Need more help? You can use the examples below, if you're still lost feel free to open an issue or a discussion! 32 | 33 | ## Examples 34 | ### Get a tweet from its ID 35 | ```cs 36 | var client = new TwitterSharp.Client.TwitterClient(bearerToken); 37 | var answer = await client.GetTweetAsync("1389189291582967809"); 38 | Console.WriteLine(answer.Text); // たのしみ!!\uD83D\uDC93 https://t.co/DgBYVYr9lN 39 | ``` 40 | 41 | ### Get an user from its username 42 | ```cs 43 | var client = new TwitterSharp.Client.TwitterClient(bearerToken); 44 | var answer = await client.GetUserAsync("theindra5"); 45 | Console.WriteLine(answer.Id); // 1022468464513089536 46 | ``` 47 | 48 | ### Get latest tweets from an user id with the attached medias 49 | ```cs 50 | var client = new TwitterSharp.Client.TwitterClient(bearerToken); 51 | // You can get the id using GetUsersAsync 52 | var answer = await client.GetTweetsFromUserIdAsync("1109748792721432577", new TweetSearchOptions 53 | { 54 | TweetOptions = new[] { TweetOption.Attachments }, 55 | MediaOptions = new[] { MediaOption.Preview_Image_Url } 56 | }); 57 | for (int i = 0; i < answer.Length; i++) 58 | { 59 | var tweet = answer[i]; 60 | Console.WriteLine($"Tweet n°{i}"); 61 | Console.WriteLine(tweet.Text); 62 | if (tweet.Attachments?.Media?.Any() ?? false) 63 | { 64 | Console.WriteLine("\nImages:"); 65 | Console.WriteLine(string.Join("\n", tweet.Attachments.Media.Select(x => x.Url))); 66 | } 67 | Console.WriteLine("\n"); 68 | } 69 | ``` 70 | 71 | ### Get the users that someone follow 72 | ```cs 73 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 74 | 75 | var answer = await client.GetFollowingAsync("1433657158067896325", new UserSearchOptions 76 | { 77 | Limit = 1000 78 | }); 79 | Console.WriteLine(string.Join("\n", answer.Users.Select(u => u.Username))); 80 | while (answer.NextAsync != null) // We go to the next page if there is one 81 | { 82 | answer = await answer.NextAsync(); 83 | Console.WriteLine(string.Join("\n", answer.Users.Select(u => u.Username))); 84 | } 85 | ``` 86 | 87 | ### Continuously get all the new tweets from some users 88 | ```cs 89 | var client = new TwitterSharp.Client.TwitterClient(bearerToken); 90 | 91 | // Subscribe to 5 Twitter accounts 92 | var request = new TwitterSharp.Request.StreamRequest( 93 | Expression.Author("moricalliope") // using TwitterSharp.Rule; 94 | .Or( 95 | Expression.Author("takanashikiara"), 96 | Expression.Author("ninomaeinanis"), 97 | Expression.Author("gawrgura"), 98 | Expression.Author("watsonameliaEN") 99 | ) 100 | , "Hololive"); 101 | await client.AddTweetStreamAsync(request); // Add them to the stream 102 | 103 | // We display all the subscriptions we have 104 | var subs = await client.GetInfoTweetStreamAsync(); 105 | Console.WriteLine("Subscriptions: " + string.Join("\n", subs.Select(x => x.Value.ToString()))); 106 | 107 | // NextTweetStreamAsync will continue to run in background 108 | Task.Run(async () => 109 | { 110 | // Take in parameter a callback called for each new tweet 111 | // Since we want to get the basic info of the tweet author, we add an empty array of UserOption 112 | await client.NextTweetStreamAsync((tweet) => 113 | { 114 | Console.WriteLine($"From {tweet.Author.Name}: {tweet.Text} (Rules: {string.Join(',', tweet.MatchingRules.Select(x => x.Tag))})"); 115 | }, 116 | new TweetSearchOptions 117 | { 118 | UserOptions = Array.Empty() 119 | }); 120 | }); 121 | 122 | // Add new high frequent rule after the stream started. No disconnection needed. 123 | await client.AddTweetStreamAsync(new TwitterSharp.Request.StreamRequest( Expression.Author("Every3Minutes"), "Frequent")); 124 | ``` 125 | 126 | ### Search the expression tree 127 | ```cs 128 | var expressionString = "(@twitterdev OR @twitterapi) -@twitter"; 129 | // Parse the string into an expression with a typed expression tree 130 | var parsedExpression = Expression.Parse(expressionString); 131 | 132 | var mentionsCount = CountExpressionsOfType(parsedExpression, ExpressionType.Mention); 133 | var groupsCount = CountExpressionsOfType(parsedExpression, ExpressionType.And) + CountExpressionsOfType(parsedExpression, ExpressionType.Or);; 134 | 135 | Console.WriteLine($"Found {mentionsCount} mentions and {groupsCount} groups in the expression"); // Found 3 mentions and 2 groups in the expression 136 | 137 | // Helper function to count recursive 138 | int CountExpressionsOfType(Expression expression, ExpressionType type) 139 | { 140 | var i = expression.Type == type ? 1 : 0; 141 | 142 | if (expression.Expressions != null) 143 | { 144 | i += expression.Expressions.Sum(exp => CountExpressionsOfType(exp, type)); 145 | } 146 | 147 | return i; 148 | } 149 | ``` 150 | 151 | ## Contributing 152 | 153 | If you want to contribute feel free to open a pull request\ 154 | You can also see how the project is going in the [project tab](https://github.com/Xwilarg/TwitterSharp/projects/1) 155 | -------------------------------------------------------------------------------- /TwitterSharp.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31025.194 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TwitterSharp", "src\TwitterSharp.csproj", "{194B217B-A4FC-458C-9891-C898FC1CC5A7}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TwitterSharp.UnitTests", "test\TwitterSharp.UnitTests.csproj", "{455E01B8-9838-4D4B-A0E5-52A5F971832B}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {194B217B-A4FC-458C-9891-C898FC1CC5A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {194B217B-A4FC-458C-9891-C898FC1CC5A7}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {194B217B-A4FC-458C-9891-C898FC1CC5A7}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {194B217B-A4FC-458C-9891-C898FC1CC5A7}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {455E01B8-9838-4D4B-A0E5-52A5F971832B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {455E01B8-9838-4D4B-A0E5-52A5F971832B}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {455E01B8-9838-4D4B-A0E5-52A5F971832B}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {455E01B8-9838-4D4B-A0E5-52A5F971832B}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {FA1A55E3-1148-49EF-B4C7-2828B4F1D53D} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /copy_coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | cd test/TestResults 4 | cd $(ls) 5 | cp coverage.cobertura.xml ../../../coverage.xml 6 | cd ../../.. -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | ############### 2 | # folder # 3 | ############### 4 | /**/DROP/ 5 | /**/TEMP/ 6 | /**/packages/ 7 | /**/bin/ 8 | /**/obj/ 9 | _site 10 | -------------------------------------------------------------------------------- /docs/api/.gitignore: -------------------------------------------------------------------------------- 1 | ############### 2 | # temp file # 3 | ############### 4 | *.yml 5 | .manifest 6 | -------------------------------------------------------------------------------- /docs/api/index.md: -------------------------------------------------------------------------------- 1 | To start with, you can have a look at the methods available on [the client](/api/TwitterSharp.Client.TwitterClient.html) -------------------------------------------------------------------------------- /docs/docfx.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": [ 3 | { 4 | "src": [ 5 | { 6 | "src": "../src", 7 | "files": ["**.csproj"] 8 | } 9 | ], 10 | "dest": "api", 11 | "disableGitFeatures": false, 12 | "disableDefaultFilter": false 13 | } 14 | ], 15 | "build": { 16 | "content": [ 17 | { 18 | "files": [ 19 | "api/**.yml", 20 | "api/index.md" 21 | ] 22 | }, 23 | { 24 | "files": [ 25 | "toc.yml", 26 | "*.md" 27 | ] 28 | } 29 | ], 30 | "resource": [ 31 | { 32 | "files": [ 33 | "images/**" 34 | ] 35 | } 36 | ], 37 | "overwrite": [ 38 | { 39 | "files": [ 40 | "apidoc/**.md" 41 | ], 42 | "exclude": [ 43 | "obj/**", 44 | "_site/**" 45 | ] 46 | } 47 | ], 48 | "dest": "_site", 49 | "globalMetadataFiles": [], 50 | "fileMetadataFiles": [], 51 | "template": [ 52 | "default" 53 | ], 54 | "postProcessors": [], 55 | "markdownEngineName": "markdig", 56 | "noLangKeyword": false, 57 | "keepFileLink": false, 58 | "cleanupCacheHistory": false, 59 | "disableGitFeatures": false 60 | } 61 | } -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # TwitterSharp 2 | TwitterSharp is a C# wrapper around the V2 of Twitter API 3 | 4 | ## Getting Started 5 | - Download the project: 6 | - [NuGet](https://www.nuget.org/packages/TwitterSharp/) 7 | - [GitHub Packages](https://github.com/Xwilarg/TwitterSharp/packages/772258) 8 | - Look at [examples in the README](https://github.com/Xwilarg/TwitterSharp#examples) 9 | - For more information, look at the [API reference](/api/index.html) 10 | -------------------------------------------------------------------------------- /docs/toc.yml: -------------------------------------------------------------------------------- 1 | - name: Api Documentation 2 | href: api/ 3 | homepage: api/index.md 4 | -------------------------------------------------------------------------------- /src/ApiEndpoint/AccessLevel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace TwitterSharp.ApiEndpoint 4 | { 5 | [Flags] 6 | public enum AccessLevel 7 | { 8 | None = 0, 9 | Essential = 1, 10 | Elevated = 2, 11 | ElevatedPlus = 4, 12 | AcademicResearch = 8, 13 | All = Essential | Elevated | ElevatedPlus | AcademicResearch 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/ApiEndpoint/Endpoint.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace TwitterSharp.ApiEndpoint 4 | { 5 | /// 6 | /// All endpoints enhanced with rate limitsall and information from , 7 | /// and 8 | /// There is also a Tweet Cap per month depending the Acceess Level: 9 | /// Essential: 500K Tweets per month / Project 10 | /// Elevated: 2M Tweets per month / Project 11 | /// Academic: 10M Tweets per month / Project 12 | /// If you have Academic Research access, you can connect up to two redundant connections to maximize your streaming up-time. 13 | /// 14 | public enum Endpoint 15 | { 16 | #region Tweets 17 | 18 | [Endpoint(Resource = Resource.Tweets, EndpointType = EndpointType.GET, Url = "/2/tweets", Group = "Tweet lookup", LimitPerApp = 300, LimitPerUser = 900)] 19 | [Description("Retrieve multiple Tweets with a list of IDs")] 20 | GetTweetsByIds, 21 | [Endpoint(Resource = Resource.Tweets, EndpointType = EndpointType.GET, Url = "/2/tweets/:id", Group = "Tweet lookup", LimitPerApp = 300, LimitPerUser = 900)] 22 | [Description("Retrieve a single Tweet with an ID")] 23 | GetTweetById, 24 | [Endpoint(Resource = Resource.Tweets, EndpointType = EndpointType.POST, Url = "/2/tweets", Group = "Manage Tweets", LimitPerUser = 200, MaxPerUser = 300, MaxResetIntervalHours = 3, AdditionalInfo = "max shared with CreateRetweet")] 25 | [Description("Post a Tweet")] 26 | CreateTweet, 27 | [Endpoint(Resource = Resource.Tweets, EndpointType = EndpointType.DELETE, Url = "/2/tweets/:id", Group = "Manage Tweets", LimitPerUser = 50)] 28 | [Description("Delete a Tweet")] 29 | DeleteTweet, 30 | [Endpoint(Resource = Resource.Tweets, EndpointType = EndpointType.GET, Url = "/2/users/:id/timelines/reverse_chronological", Group = "Timelines", LimitPerUser = 180)] 31 | [Description("Allows you to retrieve a collection of the most recent Tweets and Retweets posted by you and users you follow")] 32 | ReverseChronologicalTimeline, 33 | [Endpoint(Resource = Resource.Tweets, EndpointType = EndpointType.GET, Url = "/2/users/:id/tweets", Group = "Timelines", LimitPerApp = 1500, LimitPerUser = 900, TweetCap = true)] 34 | [Description("Returns most recent Tweets composed a specified user ID")] 35 | UserTweetTimeline, 36 | [Endpoint(Resource = Resource.Tweets, EndpointType = EndpointType.GET, Url = "/2/users/:id/mentions", Group = "Timelines", LimitPerApp = 450, LimitPerUser = 180, TweetCap = true)] 37 | [Description("Returns most recent Tweets mentioning a specified user ID")] 38 | UserMentionTimeline, 39 | [Endpoint(Resource = Resource.Tweets, EndpointType = EndpointType.GET, Url = "/2/tweets/search/recent", Group = "Search Tweets", LimitPerApp = 450, LimitPerUser = 180, TweetCap = true, AdditionalInfo = "Additional Limits for all access levels: - 10 default results per response - 100 results per response. Essential + Elevated: - core operators - 512 query length. Academic: - enhanced operators - 1024 query length")] 40 | [Description("Search for Tweets published in the last 7 days")] 41 | RecentSearch, 42 | [Endpoint(Resource = Resource.Tweets, EndpointType = EndpointType.GET, Url = "/2/tweets/search/all", Group = "Search Tweets", LimitPerApp = 300, TweetCap = true, AccessLevel = AccessLevel.AcademicResearch, AdditionalInfo = "Full-archive also has a 1 request / 1 second limit. Additional Limits: - 500 results per response - 10 default results per response - enhanced operators - 1024 query length")] 43 | [Description("Search the full archive of Tweets")] 44 | FullArchiveSearch, 45 | [Endpoint(Resource = Resource.Tweets, EndpointType = EndpointType.GET, Url = "/2/tweets/counts/recent", Group = "Tweet counts", LimitPerApp = 300, AdditionalInfo = "Essential + Elevated: - core operators - 512 query length. Academic: - enhanced operators - 1024 query length")] 46 | [Description("Receive a count of Tweets that match a query in the last 7 days")] 47 | RecentTweetCounts, 48 | [Endpoint(Resource = Resource.Tweets, EndpointType = EndpointType.GET, Url = "/2/tweets/counts/all", Group = "Tweet counts", LimitPerApp = 300, AccessLevel = AccessLevel.AcademicResearch, AdditionalInfo = "- enhanced operators, - 1024 query length")] 49 | [Description("Receive a count of Tweets that match a query")] 50 | FullArchiveTweetCounts, 51 | [Endpoint(Resource = Resource.Tweets, EndpointType = EndpointType.GET, Url = "/2/tweets/search/stream", Group = "Filtered stream", LimitPerApp = 50, TweetCap = true, AdditionalInfo = "Essential + Elevated: - core operators - 512 rule length. Essential - 5 rules. Elevated: - 25 rules. Academic: - 1000 rules - enhanced operators - 1024 query length")] 52 | [Description("Connect to the stream")] 53 | ConnectingFilteresStream, 54 | [Endpoint(Resource = Resource.Tweets, EndpointType = EndpointType.POST, Url = "/2/tweets/search/stream/rules", Group = "Filtered stream", LimitPerApp = 25, AdditionalInfo = "Essential + Elevated: - does not support backfill - 50 Tweets per second - 1 connections. Academic: - supports backfill - 2 connections - 250 Tweets per second")] 55 | [Description("Add or delete rules from your stream")] 56 | AddingDeletingFilters, 57 | [Endpoint(Resource = Resource.Tweets, EndpointType = EndpointType.GET, Url = "/2/tweets/search/stream/rules", Group = "Filtered stream", LimitPerApp = 450)] 58 | [Description("Retrieve your stream's rules")] 59 | ListingFilters, 60 | [Endpoint(Resource = Resource.Tweets, EndpointType = EndpointType.GET, Url = "/2/tweets/sample/stream", Group = "Sampled stream", LimitPerApp = 50)] 61 | [Description("Streams about 1% of all Tweets in real-time.")] 62 | SampledStream, 63 | [Endpoint(Resource = Resource.Tweets, EndpointType = EndpointType.GET, Url = "/2/tweets/:id/retweeted_by", Group = "Retweets lookup", LimitPerApp = 75, LimitPerUser = 75, TweetCap = true)] 64 | [Description("Users who have Retweeted a Tweet")] 65 | RetweetsLookup, 66 | [Endpoint(Resource = Resource.Tweets, EndpointType = EndpointType.GET, Url = "/2/tweets/:id/quote_tweets", Group = "Retweets lookup", LimitPerApp = 75, LimitPerUser = 75, TweetCap = true)] 67 | [Description("Returns Quote Tweets for a Tweet specified by the requested Tweet ID.")] 68 | QuotesLookup, 69 | [Endpoint(Resource = Resource.Tweets, EndpointType = EndpointType.POST, Url = "/2/users/:id/retweets", Group = "Manage Retweets", LimitPerUser = 50, MaxPerUser = 300, MaxResetIntervalHours = 3, AdditionalInfo = "max shared with CreateTweet")] 70 | [Description("Allows a user ID to Retweet a Tweet")] 71 | CreateRetweet, 72 | [Endpoint(Resource = Resource.Tweets, EndpointType = EndpointType.DELETE, Url = "/2/users/:id/retweets/:tweet_id", Group = "Manage Retweets", LimitPerUser = 50, MaxPerUser = 1000, MaxResetIntervalHours = 24)] 73 | [Description("Allows a user ID to undo a Retweet")] 74 | DeleteRetweet, 75 | [Endpoint(Resource = Resource.Tweets, EndpointType = EndpointType.GET, Url = "/2/users/:id/liked_tweets", Group = "Likes lookup", LimitPerApp = 75, LimitPerUser = 75, TweetCap = true)] 76 | [Description("Tweets liked by a user")] 77 | TweetsLiked, 78 | [Endpoint(Resource = Resource.Tweets, EndpointType = EndpointType.GET, Url = "/2/tweets/:id/liking_users", Group = "Likes lookup", LimitPerApp = 75, LimitPerUser = 75)] 79 | [Description("Users who have liked a Tweet")] 80 | UsersLiked, 81 | [Endpoint(Resource = Resource.Tweets, EndpointType = EndpointType.POST, Url = "/2/users/:id/likes", Group = "Manage Likes", LimitPerUser = 50, MaxPerUser = 1000, MaxResetIntervalHours = 24, AdditionalInfo = "max shared with UnlikeTweet")] 82 | [Description("Allows a user ID to like a Tweet")] 83 | LikeTweet, 84 | [Endpoint(Resource = Resource.Tweets, EndpointType = EndpointType.DELETE, Url = "/2/users/:id/likes/:tweet_id", Group = "Manage Likes", LimitPerUser = 50, MaxPerUser = 1000, MaxResetIntervalHours = 24, AdditionalInfo = "max shared with LikeTweet")] 85 | [Description("Allows a user ID to unlike a Tweet")] 86 | UnlikeTweet, 87 | [Endpoint(Resource = Resource.Tweets, EndpointType = EndpointType.PUT, Url = "/2/tweets/:tweet_id/hidden", Group = "Hide replies", LimitPerUser = 50)] 88 | [Description("Hides or unhides a reply to a Tweet.")] 89 | HideReplies, 90 | 91 | #endregion 92 | 93 | #region Users 94 | 95 | [Endpoint(Resource = Resource.Users, EndpointType = EndpointType.GET, Url = "/2/users/me", Group = "User lookup", LimitPerUser = 75)] 96 | [Description("Retrieve the currently authenticated user")] 97 | GetMe, 98 | [Endpoint(Resource = Resource.Users, EndpointType = EndpointType.GET, Url = "/2/users/by/username/:username", Group = "User lookup", LimitPerApp = 300, LimitPerUser = 900)] 99 | [Description("Retrieve a single user with a usernames")] 100 | GetUserByName, 101 | [Endpoint(Resource = Resource.Users, EndpointType = EndpointType.GET, Url = "/2/users/by", Group = "User lookup", LimitPerApp = 300, LimitPerUser = 900)] 102 | [Description("Retrieve multiple users with usernames")] 103 | GetUsersByNames, 104 | [Endpoint(Resource = Resource.Users, EndpointType = EndpointType.GET, Url = "/2/users/:id", Group = "User lookup", LimitPerApp = 300, LimitPerUser = 900)] 105 | [Description("Retrieve a single user with an ID")] 106 | GetUserById, 107 | [Endpoint(Resource = Resource.Users, EndpointType = EndpointType.GET, Url = "/2/users", Group = "User lookup", LimitPerApp = 300, LimitPerUser = 900)] 108 | [Description("Retrieve multiple users with IDs")] 109 | GetUsersByIds, 110 | [Endpoint(Resource = Resource.Users, EndpointType = EndpointType.GET, Url = "/2/users/me", Group = "User lookup", LimitPerUser = 75)] 111 | [Description("Returns the information about an authorized user")] 112 | GetUserMe, 113 | [Endpoint(Resource = Resource.Users, EndpointType = EndpointType.GET, Url = "/2/users/:id/followers", Group = "Follows lookup", LimitPerApp = 15, LimitPerUser = 15)] 114 | [Description("Lookup followers of a user by ID")] 115 | GetFollowersById, 116 | [Endpoint(Resource = Resource.Users, EndpointType = EndpointType.GET, Url = "/2/users/by/username/:username/followers", Group = "Follows lookup", LimitPerApp = 15, LimitPerUser = 15)] 117 | [Description("Lookup followers of a user by Username")] 118 | GetFollowersByName, 119 | [Endpoint(Resource = Resource.Users, EndpointType = EndpointType.GET, Url = "/2/users/:id/following", Group = "Follows lookup", LimitPerApp = 15, LimitPerUser = 15)] 120 | [Description("Lookup following of a user by ID")] 121 | GetFollowingsById, 122 | [Endpoint(Resource = Resource.Users, EndpointType = EndpointType.GET, Url = "/2/users/by/username/:username/following", Group = "Follows lookup", LimitPerApp = 15, LimitPerUser = 15)] 123 | [Description("Lookup following of a user by Username")] 124 | GetFollowingsByName, 125 | [Endpoint(Resource = Resource.Users, EndpointType = EndpointType.POST, Url = "/2/users/:id/following", Group = "Manage follows", LimitPerUser = 50, MaxPerUser = 400, MaxPerApp = 1000, MaxResetIntervalHours = 24)] 126 | [Description("Allows a user ID to follow another user")] 127 | FollowUser, 128 | [Endpoint(Resource = Resource.Users, EndpointType = EndpointType.DELETE, Url = "/2/users/:source_user_id/following/:target_user_id", Group = "Manage follows", LimitPerUser = 50, MaxPerApp = 500, MaxResetIntervalHours = 24)] 129 | [Description("Allows a user ID to unfollow another user")] 130 | UnfollowUser, 131 | [Endpoint(Resource = Resource.Users, EndpointType = EndpointType.GET, Url = "/2/users/:id/blocking", Group = "Blocks lookup", LimitPerUser = 15)] 132 | [Description("Returns a list of users who are blocked by the specified user ID")] 133 | BlocksLookup, 134 | [Endpoint(Resource = Resource.Users, EndpointType = EndpointType.POST, Url = "/2/users/:id/blocking", Group = "Manage blocks", LimitPerUser = 50)] 135 | [Description("Allows a user ID to block another user")] 136 | BlockUser, 137 | [Endpoint(Resource = Resource.Users, EndpointType = EndpointType.DELETE, Url = "/2/users/:source_user_id/blocking/:target_user_id", Group = "Manage blocks", LimitPerUser = 50)] 138 | [Description("Allows a user ID to unblock another user")] 139 | UnblockUser, 140 | [Endpoint(Resource = Resource.Users, EndpointType = EndpointType.GET, Url = "/2/users/:id/muting", Group = "Mutes lookup", LimitPerUser = 15)] 141 | [Description("Returns a list of users who are muted by the specified user ID")] 142 | MutesLookup, 143 | [Endpoint(Resource = Resource.Users, EndpointType = EndpointType.POST, Url = "/2/users/:id/muting", Group = "Manage mutes", LimitPerUser = 50)] 144 | [Description("Allows a user ID to mute another user")] 145 | MuteUser, 146 | [Endpoint(Resource = Resource.Users, EndpointType = EndpointType.DELETE, Url = "/2/users/:source_user_id/muting/:target_user_id", Group = "Manage mutes", LimitPerUser = 50)] 147 | [Description("Allows a user ID to unmute another user")] 148 | UnmuteUser, 149 | 150 | #endregion 151 | 152 | #region Spaces 153 | 154 | [Endpoint(Resource = Resource.Spaces, EndpointType = EndpointType.GET, Url = "/2/spaces/:id", Group = "Spaces lookup", LimitPerApp = 300, LimitPerUser = 300)] 155 | [Description("Lookup Space by ID")] 156 | GetSpaceById, 157 | [Endpoint(Resource = Resource.Spaces, EndpointType = EndpointType.GET, Url = "/2/spaces", Group = "Spaces lookup", LimitPerApp = 300, LimitPerUser = 300)] 158 | [Description("Lookup multiple Spaces by ID")] 159 | GetSpacesByIds, 160 | [Endpoint(Resource = Resource.Spaces, EndpointType = EndpointType.GET, Url = "/2/spaces/:id/tweets", Group = "Spaces lookup", LimitPerApp = 300, LimitPerUser = 300)] 161 | [Description("Returns Tweets shared in the requested Spaces.")] 162 | GetSpacesTweets, 163 | [Endpoint(Resource = Resource.Spaces, EndpointType = EndpointType.GET, Url = "/2/spaces/:id/buyers", Group = "Spaces lookup", LimitPerApp = 300, LimitPerUser = 300)] 164 | [Description("Get users who purchased a ticket to a Space")] 165 | GetSpaceBuyers, 166 | [Endpoint(Resource = Resource.Spaces, EndpointType = EndpointType.GET, Url = "/2/spaces/by/creator_ids", Group = "Spaces lookup", LimitPerApp = 300, LimitPerUser = 300)] 167 | [Description("Discover Spaces created by user ID")] 168 | GetSpaceByCreator, 169 | [Endpoint(Resource = Resource.Spaces, EndpointType = EndpointType.GET, Url = "/2/spaces/search", Group = "Search Spaces", LimitPerApp = 300, LimitPerUser = 300)] 170 | [Description("Return live or scheduled Spaces matching your specified search terms")] 171 | SearchSpaces, 172 | 173 | #endregion 174 | 175 | #region Lists 176 | 177 | [Endpoint(Resource = Resource.Lists, EndpointType = EndpointType.GET, Url = "/2/lists/:id", Group = "List lookup", LimitPerApp = 75, LimitPerUser = 75)] 178 | [Description("Lookup a specific list by ID")] 179 | GetListById, 180 | [Endpoint(Resource = Resource.Lists, EndpointType = EndpointType.GET, Url = "/2/users/:id/owned_lists", Group = "List lookup", LimitPerApp = 15, LimitPerUser = 15)] 181 | [Description("Lookup a user's owned List")] 182 | GetUserOwnedLists, 183 | [Endpoint(Resource = Resource.Lists, EndpointType = EndpointType.POST, Url = "/2/lists", Group = "Manage Lists", LimitPerUser = 300)] 184 | [Description("Creates a new List on behalf of an authenticated user")] 185 | CreateList, 186 | [Endpoint(Resource = Resource.Lists, EndpointType = EndpointType.DELETE, Url = "/2/lists/:id", Group = "Manage Lists", LimitPerUser = 300)] 187 | [Description("Deletes a List the authenticated user owns")] 188 | DeleteList, 189 | [Endpoint(Resource = Resource.Lists, EndpointType = EndpointType.PUT, Url = "/2/lists/:id", Group = "Manage Lists", LimitPerUser = 300)] 190 | [Description("Updates the metadata for a List the authenticated user owns")] 191 | UpdateList, 192 | [Endpoint(Resource = Resource.Lists, EndpointType = EndpointType.GET, Url = "/2/lists/:id/tweets", Group = "List Tweets lookup", LimitPerApp = 900, LimitPerUser = 900, TweetCap = true)] 193 | [Description("Lookup Tweets from a specified List")] 194 | ListsTweetsLookup, 195 | [Endpoint(Resource = Resource.Lists, EndpointType = EndpointType.GET, Url = "/2/lists/:id/members", Group = "List members", LimitPerApp = 900, LimitPerUser = 900)] 196 | [Description("Returns a list of members from a specified List")] 197 | GetListMember, 198 | [Endpoint(Resource = Resource.Lists, EndpointType = EndpointType.GET, Url = "/2/users/:id/list_memberships", Group = "List members", LimitPerApp = 75, LimitPerUser = 75)] 199 | [Description("Returns all Lists a specified user is a member of")] 200 | GetUsersList, 201 | [Endpoint(Resource = Resource.Lists, EndpointType = EndpointType.POST, Url = "/2/lists/:id/members", Group = "Manage List members", LimitPerUser = 300)] 202 | [Description("Add a member to a List that the authenticated user owns")] 203 | ListAddMember, 204 | [Endpoint(Resource = Resource.Lists, EndpointType = EndpointType.DELETE, Url = "/2/lists/:id/members/:user_id", Group = "Manage List members", LimitPerUser = 300)] 205 | [Description("Removes a member from a List the authenticated user owns")] 206 | ListRemoveMember, 207 | [Endpoint(Resource = Resource.Lists, EndpointType = EndpointType.GET, Url = "/2/lists/:id/followers", Group = "List follows", LimitPerApp = 180, LimitPerUser = 180)] 208 | [Description("Returns all followers of a specified List")] 209 | GetListFollowers, 210 | [Endpoint(Resource = Resource.Lists, EndpointType = EndpointType.GET, Url = "/2/users/:id/followed_lists", Group = "List follows", LimitPerApp = 15, LimitPerUser = 15)] 211 | [Description("Returns all Lists a specified user follows")] 212 | GetUsersFollowedList, 213 | [Endpoint(Resource = Resource.Lists, EndpointType = EndpointType.POST, Url = "/2/users/:id/followed_lists", Group = "Manage List follows", LimitPerUser = 50)] 214 | [Description("Follows a List on behalf of an authenticated user")] 215 | FollowList, 216 | [Endpoint(Resource = Resource.Lists, EndpointType = EndpointType.DELETE, Url = "/2/users/:id/followed_lists/:list_id", Group = "Manage List follows", LimitPerUser = 50)] 217 | [Description("Unfollows a List on behalf of an authenticated user")] 218 | UnfollowList, 219 | [Endpoint(Resource = Resource.Lists, EndpointType = EndpointType.GET, Url = "/2/users/:id/pinned_lists", Group = "Manage pinned Lists", LimitPerUser = 15)] 220 | [Description("Returns the pinned Lists of the authenticated user")] 221 | GetUsersPinnedLists, 222 | [Endpoint(Resource = Resource.Lists, EndpointType = EndpointType.POST, Url = "/2/users/:id/pinned_lists", Group = "Manage pinned Lists", LimitPerUser = 50)] 223 | [Description("Pins a List on behalf of an authenticated user")] 224 | PinList, 225 | [Endpoint(Resource = Resource.Lists, EndpointType = EndpointType.DELETE, Url = "/2/users/:id/pinned_lists/:list_id", Group = "Manage pinned Lists", LimitPerUser = 50)] 226 | [Description("Unpins a List on behalf of an authenticated user")] 227 | UnpinList, 228 | 229 | #endregion 230 | 231 | #region Bookmarks 232 | 233 | [Endpoint(Resource = Resource.Tweets, EndpointType = EndpointType.GET, Url = "/2/users/:id/bookmarks", Group = "Bookmarks lookup", LimitPerUser = 180)] 234 | [Description("Allows you to get an authenticated user's 800 most recent bookmarked Tweets")] 235 | BookmarksLookup, 236 | [Endpoint(Resource = Resource.Tweets, EndpointType = EndpointType.POST, Url = "/2/users/:id/bookmarks", Group = "Manage Bookmarks", LimitPerUser = 50)] 237 | [Description("Causes the user ID of an authenticated user identified in the path parameter to Bookmark the target Tweet provided in the request body")] 238 | BookmarkTweet, 239 | [Endpoint(Resource = Resource.Tweets, EndpointType = EndpointType.DELETE, Url = "/2/users/:id/bookmarks/:tweet_id", Group = "Manage Bookmarks", LimitPerUser = 50)] 240 | [Description("Allows a user or authenticated user ID to remove a Bookmark of a Tweet")] 241 | RemoveBookmark, 242 | 243 | #endregion 244 | 245 | #region Compliance 246 | 247 | [Endpoint(Resource = Resource.Compliance, EndpointType = EndpointType.POST, Url = "/2/compliance/jobs", Group = "Batch compliance", LimitPerApp = 150)] 248 | [Description("Creates a new compliance job")] 249 | CreateJob, 250 | [Endpoint(Resource = Resource.Compliance, EndpointType = EndpointType.GET, Url = "/2/compliance/jobs", Group = "Batch compliance", LimitPerApp = 150)] 251 | [Description("Returns a list of recent compliance jobs")] 252 | GetJobs, 253 | [Endpoint(Resource = Resource.Compliance, EndpointType = EndpointType.GET, Url = "/2/compliance/jobs/:job_id", Group = "Batch compliance", LimitPerApp = 150)] 254 | [Description("Returns status and download information about a specified compliance job")] 255 | GetJobById, 256 | 257 | #endregion 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /src/ApiEndpoint/EndpointAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace TwitterSharp.ApiEndpoint 4 | { 5 | public class EndpointAttribute : Attribute 6 | { 7 | public Resource Resource { get; set; } 8 | public EndpointType EndpointType { get; set; } = EndpointType.GET; 9 | public AccessLevel AccessLevel { get; set; } = AccessLevel.All; 10 | public string Url { get; set; } 11 | 12 | /// 13 | /// Certain endpoints (like filtered stream and recent search) have a limit on how many Tweets they can pull 14 | /// per month. Learn more 15 | /// 16 | public bool TweetCap { get; set; } 17 | public string Group { get; set; } 18 | public int LimitPerApp { get; set; } 19 | public int LimitPerUser { get; set; } 20 | public int LimitResetIntervalMinutes { get; set; } = 15; 21 | public int MaxPerApp { get; set; } 22 | public int MaxPerUser { get; set; } 23 | public int MaxResetIntervalHours { get; set; } 24 | public string AdditionalInfo { get; set; } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/ApiEndpoint/EndpointType.cs: -------------------------------------------------------------------------------- 1 | namespace TwitterSharp.ApiEndpoint 2 | { 3 | public enum EndpointType 4 | { 5 | GET, 6 | POST, 7 | DELETE, 8 | PUT 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/ApiEndpoint/Resource.cs: -------------------------------------------------------------------------------- 1 | namespace TwitterSharp.ApiEndpoint 2 | { 3 | public enum Resource 4 | { 5 | Tweets, 6 | Users, 7 | Lists, 8 | Spaces, 9 | Compliance 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/AssemblyAttributes.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | [assembly: CLSCompliant(true)] -------------------------------------------------------------------------------- /src/Client/TwitterClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Net.Http.Headers; 6 | using System.Net.Sockets; 7 | using System.Text; 8 | using System.Text.Json; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | using System.Web; 12 | using TwitterSharp.ApiEndpoint; 13 | using TwitterSharp.JsonOption; 14 | using TwitterSharp.Model; 15 | using TwitterSharp.Request; 16 | using TwitterSharp.Request.Internal; 17 | using TwitterSharp.Request.Option; 18 | using TwitterSharp.Response; 19 | using TwitterSharp.Response.RStream; 20 | using TwitterSharp.Response.RTweet; 21 | using TwitterSharp.Response.RUser; 22 | using TwitterSharp.Rule; 23 | 24 | namespace TwitterSharp.Client 25 | { 26 | /// 27 | /// Base client to do all your requests 28 | /// 29 | public class TwitterClient : IDisposable 30 | { 31 | /// 32 | /// Create a new instance of the client 33 | /// 34 | /// Bearer token generated from https://developer.twitter.com/ 35 | public TwitterClient(string bearerToken) 36 | { 37 | _httpClient = new(); 38 | _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken); 39 | 40 | _jsonOptions = new JsonSerializerOptions 41 | { 42 | PropertyNamingPolicy = new SnakeCaseNamingPolicy() 43 | }; 44 | _jsonOptions.Converters.Add(new EntitiesConverter()); 45 | _jsonOptions.Converters.Add(new ExpressionConverter()); 46 | _jsonOptions.Converters.Add(new ReferencedTweetConverter()); 47 | _jsonOptions.Converters.Add(new ReplySettingsConverter()); 48 | _jsonOptions.Converters.Add(new MediaConverter()); 49 | } 50 | 51 | public event EventHandler RateLimitChanged; 52 | private CancellationTokenSource _tweetStreamCancellationTokenSource; 53 | 54 | #region AdvancedParsing 55 | private static void IncludesParseUser(IHaveAuthor data, Includes includes) 56 | { 57 | data.SetAuthor(includes.Users.Where(x => x.Id == data.AuthorId).FirstOrDefault()); 58 | } 59 | 60 | private static void IncludesParseUser(IHaveAuthor[] data, Includes includes) 61 | { 62 | foreach (var d in data) 63 | { 64 | IncludesParseUser(d, includes); 65 | } 66 | } 67 | 68 | private static void IncludesParseMedias(IHaveMedia data, Includes includes) 69 | { 70 | var medias = data.GetMedia(); 71 | if (medias != null) 72 | { 73 | for (int i = 0; i < medias.Length; i++) 74 | { 75 | medias[i] = includes.Media.Where(x => x.Key == medias[i].Key).FirstOrDefault(); 76 | } 77 | } 78 | } 79 | 80 | private static void IncludesParseMedias(IHaveMedia[] data, Includes includes) 81 | { 82 | foreach (var m in data) 83 | { 84 | IncludesParseMedias(m, includes); 85 | } 86 | } 87 | 88 | private static readonly Type _authorInterface = typeof(IHaveAuthor); 89 | private static readonly Type _mediaInterface = typeof(IHaveMedia); 90 | private static readonly Type _matchingRulesInterface = typeof(IHaveMatchingRules); 91 | private static void InternalIncludesParse(Answer answer) 92 | { 93 | if (answer.Includes != null) 94 | { 95 | if (answer.Includes.Users != null && answer.Includes.Users.Any() && _authorInterface.IsAssignableFrom(typeof(T))) 96 | { 97 | var data = answer.Data; 98 | IncludesParseUser((IHaveAuthor)data, answer.Includes); 99 | answer.Data = data; 100 | } 101 | if (answer.Includes.Media != null && answer.Includes.Media.Any() && _mediaInterface.IsAssignableFrom(typeof(T))) 102 | { 103 | var data = answer.Data; 104 | IncludesParseMedias((IHaveMedia)data, answer.Includes); 105 | answer.Data = data; 106 | } 107 | if (answer.MatchingRules != null && answer.MatchingRules.Any() && _matchingRulesInterface.IsAssignableFrom(typeof(T))) 108 | { 109 | (answer.Data as IHaveMatchingRules).MatchingRules = answer.MatchingRules; 110 | } 111 | } 112 | } 113 | private static void InternalIncludesParse(Answer answer) 114 | { 115 | if (answer.Includes != null) 116 | { 117 | if (answer.Includes.Users != null && answer.Includes.Users.Any() && answer.Includes.Users.Any() && _authorInterface.IsAssignableFrom(typeof(T))) 118 | { 119 | var data = answer.Data; 120 | IncludesParseUser(data.Cast().ToArray(), answer.Includes); 121 | answer.Data = data; 122 | } 123 | if (answer.Includes.Media != null && answer.Includes.Media.Any() && _mediaInterface.IsAssignableFrom(typeof(T))) 124 | { 125 | var data = answer.Data; 126 | IncludesParseMedias(data.Cast().ToArray(), answer.Includes); 127 | answer.Data = data; 128 | } 129 | } 130 | } 131 | 132 | private T[] ParseArrayData(string json) 133 | { 134 | var answer = JsonSerializer.Deserialize>(json, _jsonOptions); 135 | if (answer.Detail != null || answer.Errors != null) 136 | { 137 | throw new TwitterException(answer); 138 | } 139 | if (answer.Data == null) 140 | { 141 | return Array.Empty(); 142 | } 143 | InternalIncludesParse(answer); 144 | return answer.Data; 145 | } 146 | 147 | private Answer ParseData(string json) 148 | { 149 | var answer = JsonSerializer.Deserialize>(json, _jsonOptions); 150 | if (answer.Detail != null || answer.Errors != null) 151 | { 152 | throw new TwitterException(answer); 153 | } 154 | InternalIncludesParse(answer); 155 | return answer; 156 | } 157 | 158 | private void BuildRateLimit(HttpResponseHeaders headers, Endpoint endpoint) 159 | { 160 | if (headers == null) 161 | { 162 | return; 163 | } 164 | 165 | var rateLimit = new RateLimit(endpoint); 166 | 167 | if (headers.TryGetValues("x-rate-limit-limit", out var limit)) 168 | { 169 | rateLimit.Limit = Convert.ToInt32(limit.FirstOrDefault()); 170 | } 171 | 172 | if (headers.TryGetValues("x-rate-limit-remaining", out var remaining)) 173 | { 174 | rateLimit.Remaining = Convert.ToInt32(remaining.FirstOrDefault()); 175 | } 176 | 177 | if (headers.TryGetValues("x-rate-limit-reset", out var reset)) 178 | { 179 | rateLimit.Reset = Convert.ToInt32(reset.FirstOrDefault()); 180 | } 181 | 182 | RateLimitChanged?.Invoke(this, rateLimit); 183 | } 184 | 185 | #endregion AdvancedParsing 186 | 187 | #region TweetSearch 188 | 189 | /// 190 | /// Get a tweet given its ID 191 | /// 192 | /// ID of the tweet 193 | public async Task GetTweetAsync(string id, TweetSearchOptions options = null) 194 | { 195 | options ??= new(); 196 | var res = await _httpClient.GetAsync(_baseUrl + "tweets/" + HttpUtility.UrlEncode(id) + "?" + options.Build(true)); 197 | BuildRateLimit(res.Headers, Endpoint.GetTweetById); 198 | return ParseData(await res.Content.ReadAsStringAsync()).Data; 199 | } 200 | 201 | /// 202 | /// Get a list of tweet given their IDs 203 | /// 204 | /// All the IDs you want the tweets of 205 | public async Task GetTweetsAsync(string[] ids, TweetSearchOptions options = null) 206 | { 207 | options ??= new(); 208 | var res = await _httpClient.GetAsync(_baseUrl + "tweets?ids=" + string.Join(",", ids.Select(x => HttpUtility.UrlEncode(x))) + "&" + options.Build(true)); 209 | BuildRateLimit(res.Headers, Endpoint.GetTweetsByIds); 210 | return ParseArrayData(await res.Content.ReadAsStringAsync()); 211 | } 212 | 213 | /// 214 | /// Get the latest tweets of an user 215 | /// 216 | /// Username of the user you want the tweets of 217 | public async Task GetTweetsFromUserIdAsync(string userId, TweetSearchOptions options = null) 218 | { 219 | options ??= new(); 220 | var res = await _httpClient.GetAsync(_baseUrl + "users/" + HttpUtility.HtmlEncode(userId) + "/tweets?" + options.Build(true)); 221 | BuildRateLimit(res.Headers, Endpoint.UserTweetTimeline); 222 | return ParseArrayData(await res.Content.ReadAsStringAsync()); 223 | } 224 | 225 | /// 226 | /// Get the latest tweets for an expression 227 | /// 228 | /// An expression to build the query 229 | /// properties send with the tweet 230 | public async Task GetRecentTweets(Expression expression, TweetSearchOptions options = null) 231 | { 232 | options ??= new(); 233 | var res = await _httpClient.GetAsync(_baseUrl + "tweets/search/recent?query=" + HttpUtility.UrlEncode(expression.ToString()) + "&" + options.Build(true)); 234 | BuildRateLimit(res.Headers, Endpoint.RecentSearch); 235 | return ParseArrayData(await res.Content.ReadAsStringAsync()); 236 | } 237 | 238 | /// 239 | /// This endpoint is only available to those users who have been approved for Academic Research access. 240 | /// The full-archive search endpoint returns the complete history of public Tweets matching a search query; since the first Tweet was created March 26, 2006. 241 | /// 242 | /// An expression to build the query 243 | /// properties send with the tweet 244 | public async Task GetAllTweets(Expression expression, TweetSearchOptions options = null) 245 | { 246 | options ??= new(); 247 | var res = await _httpClient.GetAsync(_baseUrl + "tweets/search/all?query=" + HttpUtility.UrlEncode(expression.ToString()) + "&" + options.Build(true)); 248 | BuildRateLimit(res.Headers, Endpoint.FullArchiveSearch); 249 | return ParseArrayData(await res.Content.ReadAsStringAsync()); 250 | } 251 | 252 | #endregion TweetSearch 253 | 254 | #region TweetStream 255 | 256 | public async Task GetInfoTweetStreamAsync() 257 | { 258 | var res = await _httpClient.GetAsync(_baseUrl + "tweets/search/stream/rules"); 259 | BuildRateLimit(res.Headers, Endpoint.ListingFilters); 260 | return ParseArrayData(await res.Content.ReadAsStringAsync()); 261 | } 262 | 263 | private StreamReader _reader; 264 | private static readonly object _streamLock = new(); 265 | public static bool IsTweetStreaming { get; private set;} 266 | 267 | /// 268 | /// The stream is only meant to be open one time. So calling this method multiple time will result in an exception. 269 | /// For changing the rules with and 270 | /// "No disconnection needed to add/remove rules using rules endpoint." 271 | /// It has to be canceled with 272 | /// 273 | /// The action which is called when a tweet arrives 274 | /// Properties send with the tweet 275 | /// User properties send with the tweet 276 | /// Media properties send with the tweet 277 | /// 278 | public async Task NextTweetStreamAsync(Action onNextTweet, TweetSearchOptions options = null) 279 | { 280 | options ??= new(); 281 | 282 | lock (_streamLock) 283 | { 284 | if (IsTweetStreaming) 285 | { 286 | throw new TwitterException("Stream already running. Please cancel the stream with CancelTweetStream", "TooManyConnections", "https://api.twitter.com/2/problems/streaming-connection"); 287 | } 288 | 289 | IsTweetStreaming = true; 290 | } 291 | 292 | _tweetStreamCancellationTokenSource = new(); 293 | var res = await _httpClient.GetAsync(_baseUrl + "tweets/search/stream?" + options.Build(true), HttpCompletionOption.ResponseHeadersRead, _tweetStreamCancellationTokenSource.Token); 294 | BuildRateLimit(res.Headers, Endpoint.ConnectingFilteresStream); 295 | _reader = new(await res.Content.ReadAsStreamAsync(_tweetStreamCancellationTokenSource.Token)); 296 | 297 | try 298 | { 299 | while (!_reader.EndOfStream && !_tweetStreamCancellationTokenSource.IsCancellationRequested) 300 | { 301 | var str = await _reader.ReadLineAsync(); 302 | // Keep-alive signals: At least every 20 seconds, the stream will send a keep-alive signal, or heartbeat in the form of an \r\n carriage return through the open connection to prevent your client from timing out. Your client application should be tolerant of the \r\n characters in the stream. 303 | if (string.IsNullOrWhiteSpace(str)) 304 | { 305 | continue; 306 | } 307 | onNextTweet(ParseData(str).Data); 308 | } 309 | } 310 | catch (IOException e) 311 | { 312 | if(!(e.InnerException is SocketException se && se.SocketErrorCode == SocketError.ConnectionAborted)) 313 | { 314 | throw; 315 | } 316 | } 317 | 318 | CancelTweetStream(); 319 | } 320 | 321 | /// 322 | /// Closes the tweet stream started by . 323 | /// 324 | /// If true, the stream will be closed immediately. With falls the thread had to wait for the next keep-alive signal (every 20 seconds) 325 | public void CancelTweetStream(bool force = true) 326 | { 327 | _tweetStreamCancellationTokenSource?.Dispose(); 328 | 329 | if (force) 330 | { 331 | _reader?.Close(); 332 | _reader?.Dispose(); 333 | } 334 | 335 | IsTweetStreaming = false; 336 | } 337 | 338 | /// 339 | /// Adds rules for the tweets/search/stream endpoint, which could be subscribed with the method. 340 | /// No disconnection needed to add/remove rules using rules endpoint. 341 | /// 342 | /// The rules to be added 343 | /// The added rule 344 | public async Task AddTweetStreamAsync(params StreamRequest[] request) 345 | { 346 | var content = new StringContent(JsonSerializer.Serialize(new StreamRequestAdd { Add = request }, _jsonOptions), Encoding.UTF8, "application/json"); 347 | var res = await _httpClient.PostAsync(_baseUrl + "tweets/search/stream/rules", content); 348 | BuildRateLimit(res.Headers, Endpoint.AddingDeletingFilters); 349 | return ParseArrayData(await res.Content.ReadAsStringAsync()); 350 | } 351 | 352 | /// 353 | /// Removes a rule for the tweets/search/stream endpoint, which could be subscribed with the method. 354 | /// No disconnection needed to add/remove rules using rules endpoint. 355 | /// 356 | /// Id of the rules to be removed 357 | /// The number of deleted rules 358 | public async Task DeleteTweetStreamAsync(params string[] ids) 359 | { 360 | var content = new StringContent(JsonSerializer.Serialize(new StreamRequestDelete { Delete = new StreamRequestDeleteIds { Ids = ids } }, _jsonOptions), Encoding.UTF8, "application/json"); 361 | var res = await _httpClient.PostAsync(_baseUrl + "tweets/search/stream/rules", content); 362 | BuildRateLimit(res.Headers, Endpoint.AddingDeletingFilters); 363 | return ParseData(await res.Content.ReadAsStringAsync()).Meta.Summary.Deleted; 364 | } 365 | #endregion TweetStream 366 | 367 | #region UserSearch 368 | /// 369 | /// Gets the currently authorized user 370 | /// 371 | public async Task GetMeAsync(UserSearchOptions options = null) 372 | { 373 | options ??= new(); 374 | var res = await _httpClient.GetAsync(_baseUrl + "users/me" + "?" + options.Build(false)); 375 | BuildRateLimit(res.Headers, Endpoint.GetMe); 376 | return ParseData(await res.Content.ReadAsStringAsync()).Data; 377 | } 378 | 379 | /// 380 | /// Get an user given his username 381 | /// 382 | /// Username of the user you want information about 383 | public async Task GetUserAsync(string username, UserSearchOptions options = null) 384 | { 385 | options ??= new(); 386 | var res = await _httpClient.GetAsync(_baseUrl + "users/by/username/" + HttpUtility.UrlEncode(username) + "?" + options.Build(false)); 387 | BuildRateLimit(res.Headers, Endpoint.GetUserByName); 388 | return ParseData(await res.Content.ReadAsStringAsync()).Data; 389 | } 390 | 391 | /// 392 | /// Get a list of users given their usernames 393 | /// 394 | /// Usernames of the users you want information about 395 | public async Task GetUsersAsync(string[] usernames, UserSearchOptions options = null) 396 | { 397 | options ??= new(); 398 | var res = await _httpClient.GetAsync(_baseUrl + $"users/by?usernames={string.Join(",", usernames.Select(x => HttpUtility.UrlEncode(x)))}&{options.Build(false)}"); 399 | BuildRateLimit(res.Headers, Endpoint.GetUsersByNames); 400 | return ParseArrayData(await res.Content.ReadAsStringAsync()); 401 | } 402 | 403 | /// 404 | /// Get an user given his ID 405 | /// 406 | /// ID of the user you want information about 407 | public async Task GetUserByIdAsync(string id, UserSearchOptions options = null) 408 | { 409 | options ??= new(); 410 | var res = await _httpClient.GetAsync(_baseUrl + $"users/{HttpUtility.UrlEncode(id)}?{options.Build(false)}"); 411 | BuildRateLimit(res.Headers, Endpoint.GetUserById); 412 | return ParseData(await res.Content.ReadAsStringAsync()).Data; 413 | } 414 | 415 | /// 416 | /// Get a list of user given their IDs 417 | /// 418 | /// IDs of the user you want information about 419 | public async Task GetUsersByIdsAsync(string[] ids, UserSearchOptions options = null) 420 | { 421 | options ??= new(); 422 | var res = await _httpClient.GetAsync(_baseUrl + $"users?ids={string.Join(",", ids.Select(x => HttpUtility.UrlEncode(x)))}&{options.Build(false)}"); 423 | BuildRateLimit(res.Headers, Endpoint.GetUsersByIds); 424 | return ParseArrayData(await res.Content.ReadAsStringAsync()); 425 | } 426 | 427 | #endregion UserSearch 428 | 429 | #region GetUsers 430 | 431 | /// 432 | /// General method for getting the next page of users 433 | /// 434 | /// 435 | private async Task NextUsersAsync(string baseQuery, string token, Endpoint endpoint) 436 | { 437 | var res = await _httpClient.GetAsync(baseQuery + (!baseQuery.EndsWith("?") ? "&" : "") + "pagination_token=" + token); 438 | var data = ParseData(await res.Content.ReadAsStringAsync()); 439 | BuildRateLimit(res.Headers, endpoint); 440 | return new() 441 | { 442 | Users = data.Data, 443 | NextAsync = data.Meta.NextToken == null ? null : async () => await NextUsersAsync(baseQuery, data.Meta.NextToken, endpoint) 444 | }; 445 | } 446 | 447 | /// 448 | /// Get the follower of an user 449 | /// 450 | /// ID of the user 451 | /// Max number of result, max is 1000 452 | public async Task GetFollowersAsync(string id, UserSearchOptions options = null) 453 | { 454 | options ??= new(); 455 | var query = _baseUrl + $"users/{HttpUtility.UrlEncode(id)}/followers?{options.Build(false)}"; 456 | var res = await _httpClient.GetAsync(query); 457 | var data = ParseData(await res.Content.ReadAsStringAsync()); 458 | BuildRateLimit(res.Headers, Endpoint.GetFollowersById); 459 | return new() 460 | { 461 | Users = data.Data, 462 | NextAsync = data.Meta.NextToken == null ? null : async () => await NextUsersAsync(query, data.Meta.NextToken, Endpoint.GetFollowersById) 463 | }; 464 | } 465 | 466 | /// 467 | /// Get the following of an user 468 | /// 469 | /// ID of the user 470 | /// Max number of result, max is 1000 471 | public async Task GetFollowingAsync(string id, UserSearchOptions options = null) 472 | { 473 | options ??= new(); 474 | var query = _baseUrl + $"users/{HttpUtility.UrlEncode(id)}/following?{options.Build(false)}"; 475 | var res = await _httpClient.GetAsync(query); 476 | var data = ParseData(await res.Content.ReadAsStringAsync()); 477 | BuildRateLimit(res.Headers, Endpoint.GetFollowingsById); 478 | return new() 479 | { 480 | Users = data.Data, 481 | NextAsync = data.Meta.NextToken == null ? null : async () => await NextUsersAsync(query, data.Meta.NextToken, Endpoint.GetFollowingsById) 482 | }; 483 | } 484 | 485 | /// 486 | /// Get the likes of a tweet 487 | /// 488 | /// ID of the tweet 489 | /// This parameter enables you to select which specific user fields will deliver with each returned users objects. You can also set a Limit per page. Max is 100 490 | public async Task GetLikesAsync(string id, UserSearchOptions options = null) 491 | { 492 | options ??= new(); 493 | var query = _baseUrl + $"tweets/{HttpUtility.UrlEncode(id)}/liking_users?{options.Build(false)}"; 494 | var res = await _httpClient.GetAsync(query); 495 | var data = ParseData(await res.Content.ReadAsStringAsync()); 496 | BuildRateLimit(res.Headers, Endpoint.UsersLiked); 497 | return new() 498 | { 499 | Users = data.Data, 500 | NextAsync = data.Meta.NextToken == null ? null : async () => await NextUsersAsync(query, data.Meta.NextToken, Endpoint.UsersLiked) 501 | }; 502 | } 503 | 504 | /// 505 | /// Get the retweets of a tweet 506 | /// 507 | /// ID of the tweet 508 | /// This parameter enables you to select which specific user fields will deliver with each returned users objects. You can also set a Limit per page. Max is 100 509 | public async Task GetRetweetsAsync(string id, UserSearchOptions options = null) 510 | { 511 | options ??= new(); 512 | var query = _baseUrl + $"tweets/{HttpUtility.UrlEncode(id)}/retweeted_by?{options.Build(false)}"; 513 | var res = await _httpClient.GetAsync(query); 514 | var data = ParseData(await res.Content.ReadAsStringAsync()); 515 | BuildRateLimit(res.Headers, Endpoint.RetweetsLookup); 516 | return new() 517 | { 518 | Users = data.Data, 519 | NextAsync = data.Meta.NextToken == null ? null : async () => await NextUsersAsync(query, data.Meta.NextToken, Endpoint.RetweetsLookup) 520 | }; 521 | } 522 | 523 | #endregion Users 524 | 525 | private const string _baseUrl = "https://api.twitter.com/2/"; 526 | 527 | private readonly HttpClient _httpClient; 528 | private readonly JsonSerializerOptions _jsonOptions; 529 | 530 | public void Dispose() 531 | { 532 | CancelTweetStream(); 533 | _httpClient?.Dispose(); 534 | GC.SuppressFinalize(this); 535 | } 536 | } 537 | } 538 | -------------------------------------------------------------------------------- /src/Client/TwitterException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using TwitterSharp.Response; 3 | 4 | namespace TwitterSharp.Client 5 | { 6 | public class TwitterException : Exception 7 | { 8 | internal TwitterException(string message, string title = default, string type = default, Error[] errors = null) : base(message == null && errors != null ? "Error. See Errors property." : message) 9 | { 10 | Title = title; 11 | Type = type ?? "Error"; 12 | Errors = errors; 13 | } 14 | 15 | internal TwitterException(BaseAnswer answer) : this(answer.Detail, answer.Title, answer.Type, answer.Errors) {} 16 | 17 | public string Title { init; get; } 18 | public string Type { init; get; } 19 | public Error[] Errors { init; get; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/JsonOption/EntitiesConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | using TwitterSharp.Response.Entity; 5 | 6 | namespace TwitterSharp.JsonOption 7 | { 8 | internal class EntitiesConverter : JsonConverter 9 | { 10 | public override Entities Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 11 | { 12 | var json = JsonSerializer.Deserialize(ref reader, options); 13 | 14 | if (!json.TryGetProperty("description", out JsonElement elem)) 15 | { 16 | elem = json; 17 | } 18 | 19 | var entities = JsonSerializer.Deserialize(elem.ToString(), new JsonSerializerOptions 20 | { 21 | PropertyNameCaseInsensitive = true 22 | }); 23 | 24 | entities.Urls ??= Array.Empty(); 25 | entities.Hashtags ??= Array.Empty(); 26 | entities.Cashtags ??= Array.Empty(); 27 | entities.Mentions ??= Array.Empty(); 28 | return entities; 29 | } 30 | 31 | public override void Write(Utf8JsonWriter writer, Entities value, JsonSerializerOptions options) 32 | { 33 | JsonSerializer.Serialize(writer, value, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/JsonOption/ExpressionConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | using TwitterSharp.Rule; 5 | 6 | namespace TwitterSharp.JsonOption 7 | { 8 | internal class ExpressionConverter : JsonConverter 9 | { 10 | public override Expression Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 11 | { 12 | return new(reader.GetString(), ""); 13 | } 14 | 15 | public override void Write(Utf8JsonWriter writer, Expression value, JsonSerializerOptions options) 16 | { 17 | writer.WriteStringValue(value.ToString()); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/JsonOption/MediaConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | using TwitterSharp.Response.RMedia; 5 | 6 | namespace TwitterSharp.JsonOption 7 | { 8 | internal class MediaConverter : JsonConverter 9 | { 10 | private JsonElement? TryGetProperty(JsonElement json, string key) 11 | { 12 | if (json.TryGetProperty(key, out JsonElement elem)) 13 | { 14 | return elem; 15 | } 16 | return null; 17 | } 18 | 19 | public override Media Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 20 | { 21 | try 22 | { 23 | return new Media 24 | { 25 | Key = reader.GetString() 26 | }; 27 | } 28 | catch (InvalidOperationException) 29 | { 30 | var elem = JsonSerializer.Deserialize(ref reader, new JsonSerializerOptions 31 | { 32 | PropertyNamingPolicy = new SnakeCaseNamingPolicy() 33 | }); 34 | var media = new Media 35 | { 36 | Key = elem.GetProperty("media_key").GetString(), 37 | Type = elem.GetProperty("type").GetString() switch 38 | { 39 | "video" => MediaType.Video, 40 | "animated_gif" => MediaType.AnimatedGif, 41 | "photo" => MediaType.Photo, 42 | _ => throw new InvalidOperationException("Invalid type"), 43 | }, 44 | Height = TryGetProperty(elem, "height")?.GetInt32(), 45 | Width = TryGetProperty(elem, "width")?.GetInt32(), 46 | DurationMs = TryGetProperty(elem, "duration_ms")?.GetInt32(), 47 | PreviewImageUrl = TryGetProperty(elem, "preview_image_url")?.GetString(), 48 | Url = TryGetProperty(elem, "url")?.GetString(), 49 | Variants = GetMediaVariants(elem) 50 | }; 51 | 52 | return media; 53 | } 54 | } 55 | 56 | private MediaVariant[] GetMediaVariants(JsonElement mediaElement) 57 | { 58 | var mediaVariants = Array.Empty(); 59 | 60 | if(mediaElement.TryGetProperty("variants", out var variants)) 61 | { 62 | mediaVariants = JsonSerializer.Deserialize(variants.GetRawText(), new JsonSerializerOptions 63 | { 64 | PropertyNamingPolicy = new SnakeCaseNamingPolicy() 65 | }); 66 | } 67 | 68 | return mediaVariants; 69 | } 70 | 71 | public override void Write(Utf8JsonWriter writer, Media value, JsonSerializerOptions options) 72 | { 73 | throw new NotImplementedException(); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/JsonOption/ReferencedTweetConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | using TwitterSharp.Response.RTweet; 5 | 6 | namespace TwitterSharp.JsonOption 7 | { 8 | internal class ReferencedTweetConverter : JsonConverter 9 | { 10 | public override ReferencedTweet Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 11 | { 12 | var json = JsonSerializer.Deserialize(ref reader, options); 13 | var t = new ReferencedTweet(); 14 | switch (json.GetProperty("type").GetString()) 15 | { 16 | case "replied_to": 17 | t.Type = ReferenceType.RepliedTo; 18 | break; 19 | 20 | case "quoted": 21 | t.Type = ReferenceType.Quoted; 22 | break; 23 | 24 | case "retweeted": 25 | t.Type = ReferenceType.Retweeted; 26 | break; 27 | 28 | default: 29 | throw new InvalidOperationException("Invalid type " + json.GetProperty("type").GetString()); 30 | } 31 | t.Id = json.GetProperty("id").GetString(); 32 | return t; 33 | } 34 | 35 | public override void Write(Utf8JsonWriter writer, ReferencedTweet value, JsonSerializerOptions options) 36 | { 37 | throw new NotImplementedException(); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/JsonOption/ReplySettingsConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | using TwitterSharp.Response.RTweet; 5 | 6 | namespace TwitterSharp.JsonOption 7 | { 8 | internal class ReplySettingsConverter : JsonConverter 9 | { 10 | public override ReplySettings Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 11 | { 12 | switch (reader.GetString()) 13 | { 14 | case "everyone": 15 | return ReplySettings.Everyone; 16 | 17 | case "mentionedUsers": 18 | return ReplySettings.MentionnedUsers; 19 | 20 | case "following": 21 | return ReplySettings.Followers; 22 | 23 | default: 24 | throw new InvalidOperationException("Invalid type"); 25 | } 26 | } 27 | 28 | public override void Write(Utf8JsonWriter writer, ReplySettings value, JsonSerializerOptions options) 29 | { 30 | throw new NotImplementedException(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/JsonOption/SnakeCaseNamingPolicy.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Text; 3 | using System.Text.Json; 4 | 5 | namespace TwitterSharp.JsonOption 6 | { 7 | internal class SnakeCaseNamingPolicy : JsonNamingPolicy 8 | { 9 | public override string ConvertName(string name) 10 | { 11 | if (string.IsNullOrWhiteSpace(name)) 12 | { 13 | return name; 14 | } 15 | StringBuilder str = new(); 16 | str.Append(char.ToLower(name[0])); 17 | foreach (char c in name.Skip(1)) 18 | { 19 | if (char.IsUpper(c)) 20 | { 21 | str.Append("_" + char.ToLower(c)); 22 | } 23 | else 24 | { 25 | str.Append(c); 26 | } 27 | } 28 | return str.ToString(); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Model/IHaveAuthor.cs: -------------------------------------------------------------------------------- 1 | using TwitterSharp.Response.RUser; 2 | 3 | namespace TwitterSharp.Model 4 | { 5 | interface IHaveAuthor 6 | { 7 | internal void SetAuthor(User author); 8 | internal string AuthorId { get; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Model/IHaveMatchingRules.cs: -------------------------------------------------------------------------------- 1 | using TwitterSharp.Response.RTweet; 2 | 3 | namespace TwitterSharp.Model 4 | { 5 | interface IHaveMatchingRules 6 | { 7 | public MatchingRule[] MatchingRules { set; get; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Model/IHaveMedia.cs: -------------------------------------------------------------------------------- 1 | using TwitterSharp.Response.RMedia; 2 | 3 | namespace TwitterSharp.Model 4 | { 5 | interface IHaveMedia 6 | { 7 | internal Media[] GetMedia(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Request/AdvancedSearch/MediaOption.cs: -------------------------------------------------------------------------------- 1 | namespace TwitterSharp.Request.AdvancedSearch 2 | { 3 | public enum MediaOption 4 | { 5 | /// 6 | /// Contains the duration of the media if it's a video 7 | /// 8 | Duration_Ms, 9 | 10 | /// 11 | /// Height of the content in pixel 12 | /// 13 | Height, 14 | 15 | /// 16 | /// Width of the content in pixel 17 | /// 18 | Width, 19 | 20 | /// 21 | /// URL to a preview image of the content 22 | /// 23 | Preview_Image_Url, 24 | 25 | /// 26 | /// URL to the image of the content 27 | /// 28 | Url, 29 | 30 | /// 31 | /// Each media object may have multiple display or playback variants, with different resolutions or formats. 32 | /// For videos, each variant will also contain URLs to the video in each format. 33 | /// 34 | Variants 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Request/AdvancedSearch/TweetOption.cs: -------------------------------------------------------------------------------- 1 | namespace TwitterSharp.Request.AdvancedSearch 2 | { 3 | // https://developer.twitter.com/en/docs/twitter-api/data-dictionary/object-model/tweet 4 | public enum TweetOption 5 | { 6 | // TODO: 7 | // context_annotations 8 | // geo 9 | // non_public_metrics 10 | // organic_metrics 11 | // promoted_metrics 12 | 13 | /// 14 | /// The ID of the tweet this one is replying to 15 | /// 16 | Conversation_Id, 17 | 18 | /// 19 | /// Datetime when the tweet was crated 20 | /// 21 | Created_At, 22 | 23 | /// 24 | /// Entities in the tweet 25 | /// 26 | Entities, 27 | 28 | /// 29 | /// In the case of a reply, will contains the original user id 30 | /// 31 | In_Reply_To_User_Id, 32 | 33 | /// 34 | /// Language of the tweet, detect by Twitter 35 | /// 36 | Lang, 37 | 38 | /// 39 | /// If contains a link, indicate if it might lead to a sensitive content 40 | /// 41 | Possibly_Sensitive, 42 | 43 | /// 44 | /// Contains the following information: number of likes, number of retweets, number of replies, number of quotes 45 | /// 46 | Public_Metrics, 47 | 48 | /// 49 | /// Number of tweet this one reference 50 | /// 51 | Referenced_Tweets, 52 | 53 | /// 54 | /// Show who can reply to this tweet 55 | /// 56 | Reply_Settings, 57 | 58 | /// 59 | /// Polls and medias attached to the tweet 60 | /// Also get their ID 61 | /// 62 | Attachments, 63 | 64 | /// 65 | /// Same at "Attachments" but only get their ID 66 | /// 67 | Attachments_Ids, 68 | 69 | /// 70 | /// You can get information such as whether a Tweet was editable at the time it was created, how much time, 71 | /// if any, is remaining for a Tweet to be edited, and how many edits remain by specifying edit_controls 72 | /// in your tweet.fields parameter. 73 | /// 74 | Edit_Controls 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Request/AdvancedSearch/UserOption.cs: -------------------------------------------------------------------------------- 1 | namespace TwitterSharp.Request.AdvancedSearch 2 | { 3 | // https://developer.twitter.com/en/docs/twitter-api/data-dictionary/object-model/user 4 | public enum UserOption 5 | { 6 | /// 7 | /// UTC Datetime that the user account was created 8 | /// 9 | Created_At, 10 | 11 | /// 12 | /// Text on user's profile 13 | /// 14 | Description, 15 | 16 | /// 17 | /// Entities in the description 18 | /// 19 | Entities, 20 | 21 | /// 22 | /// Location specified on user's profile 23 | /// 24 | Location, 25 | 26 | /// 27 | /// ID of the tweet pinned on top of the user's profile 28 | /// 29 | Pinned_Tweet_Id, 30 | 31 | /// 32 | /// Url of the profile image 33 | /// 34 | Profile_Image_Url, 35 | 36 | /// 37 | /// Boolean specifying if the user's tweets are visible or not 38 | /// 39 | Protected, 40 | 41 | /// 42 | /// Contains the following information: number of followers, number of following, numbed of tweet, number of "listed" 43 | /// 44 | Public_Metrics, 45 | 46 | /// 47 | /// Url specified on the user's profile 48 | /// 49 | Url, 50 | 51 | /// 52 | /// Boolean telling if the account is marked as "verified" 53 | /// 54 | Verified, 55 | 56 | /// 57 | /// indicates the type of verification a user account has: blue, business, government or none 58 | /// 59 | Verified_Type, 60 | 61 | // Withheld // Not implemented yet 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Request/Internal/StreamRequestAdd.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace TwitterSharp.Request.Internal 4 | { 5 | internal class StreamRequestAdd 6 | { 7 | [JsonPropertyName("add")] 8 | public StreamRequest[] Add { init; get; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Request/Internal/StreamRequestDelete.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace TwitterSharp.Request.Internal 4 | { 5 | internal class StreamRequestDelete 6 | { 7 | [JsonPropertyName("delete")] 8 | public StreamRequestDeleteIds Delete { init; get; } 9 | } 10 | 11 | internal class StreamRequestDeleteIds 12 | { 13 | [JsonPropertyName("ids")] 14 | public string[] Ids { init; get; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Request/Option/ASearchOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using TwitterSharp.Request.AdvancedSearch; 4 | 5 | namespace TwitterSharp.Request.Option 6 | { 7 | public abstract class ASearchOptions 8 | { 9 | /// 10 | /// Max number of results returned by the API or the maximum number of results to be returned per page 11 | /// 12 | public int? Limit { set; get; } 13 | 14 | protected abstract void PreBuild(bool needExpansion); 15 | internal string Build(bool needExpansion) 16 | { 17 | PreBuild(needExpansion); 18 | var url = string.Join("&", _options.Select(x => x.Key + "=" + string.Join(",", x.Value))); 19 | if (Limit.HasValue) 20 | { 21 | url += $"&max_results={Limit}"; 22 | } 23 | return url; 24 | } 25 | 26 | protected void AddMediaOptions(MediaOption[] options) 27 | { 28 | if (options != null) 29 | { 30 | AddOptions("media.fields", options.Select(x => x.ToString().ToLowerInvariant())); 31 | } 32 | } 33 | 34 | /// False is we are requesting a tweet, else true 35 | protected void AddUserOptions(UserOption[] options, bool needExpansion) 36 | { 37 | if (options != null) 38 | { 39 | if (needExpansion) 40 | { 41 | AddOption("expansions", "author_id"); 42 | } 43 | AddOptions("user.fields", options.Select(x => x.ToString().ToLowerInvariant())); 44 | } 45 | } 46 | 47 | protected void AddTweetOptions(TweetOption[] options) 48 | { 49 | 50 | if (options != null) 51 | { 52 | if (options.Contains(TweetOption.Attachments)) 53 | { 54 | AddOption("expansions", "attachments.media_keys"); 55 | } 56 | AddOptions("tweet.fields", options.Select(x => 57 | { 58 | if (x == TweetOption.Attachments_Ids) 59 | { 60 | return "attachments"; 61 | } 62 | return x.ToString().ToLowerInvariant(); 63 | })); 64 | } 65 | } 66 | 67 | private void AddOption(string key, string value) 68 | { 69 | if (!_options.ContainsKey(key)) 70 | { 71 | _options.Add(key, new() { value }); 72 | } 73 | else if (!_options[key].Contains(value)) 74 | { 75 | _options[key].Add(value); 76 | } 77 | } 78 | 79 | private void AddOptions(string key, IEnumerable values) 80 | { 81 | if (!_options.ContainsKey(key)) 82 | { 83 | _options.Add(key, values.ToList()); 84 | } 85 | else 86 | { 87 | _options[key].AddRange(values); 88 | } 89 | _options[key] = _options[key].Distinct().ToList(); 90 | } 91 | 92 | protected readonly Dictionary> _options = new(); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Request/Option/TweetSearchOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using TwitterSharp.Request.AdvancedSearch; 3 | 4 | namespace TwitterSharp.Request.Option 5 | { 6 | public class TweetSearchOptions : ASearchOptions 7 | { 8 | /// 9 | /// Additional information to get about the users 10 | /// 11 | public UserOption[] UserOptions { set; get; } 12 | 13 | /// 14 | /// Additional information to get about the tweets 15 | /// 16 | public TweetOption[] TweetOptions { set; get; } 17 | 18 | /// 19 | /// Additional information to get about the medias attached 20 | /// 21 | public MediaOption[] MediaOptions { set; get; } 22 | 23 | /// 24 | /// Only returns tweet that were sent before the referenced id 25 | /// 26 | public string SinceId { set; get; } 27 | 28 | /// 29 | /// Only returns tweet that were sent after the given date 30 | /// 31 | public DateTime? StartTime { set; get; } 32 | 33 | protected override void PreBuild(bool needExpansion) 34 | { 35 | AddUserOptions(UserOptions, needExpansion); 36 | AddTweetOptions(TweetOptions); 37 | AddMediaOptions(MediaOptions); 38 | if (SinceId != null) 39 | { 40 | _options.Add("since_id", new() { SinceId }); 41 | } 42 | if (StartTime.HasValue) 43 | { 44 | _options.Add("start_time", new() { StartTime.Value.ToString("yyyy-MM-ddTHH:mm:ssZ") }); 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Request/Option/UserSearchOptions.cs: -------------------------------------------------------------------------------- 1 | using TwitterSharp.Request.AdvancedSearch; 2 | 3 | namespace TwitterSharp.Request.Option 4 | { 5 | public class UserSearchOptions : ASearchOptions 6 | { 7 | /// 8 | /// Additional information to get about the users 9 | /// 10 | public UserOption[] UserOptions { set; get; } 11 | 12 | protected override void PreBuild(bool needExpansion) 13 | { 14 | AddUserOptions(UserOptions, needExpansion); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Request/StreamRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using TwitterSharp.Rule; 3 | 4 | namespace TwitterSharp.Request 5 | { 6 | public class StreamRequest 7 | { 8 | public StreamRequest(Expression value, string tag) 9 | { 10 | Value = value; 11 | Tag = tag; 12 | } 13 | 14 | public StreamRequest(Expression value) 15 | { 16 | Value = value; 17 | Tag = ""; 18 | } 19 | 20 | [JsonPropertyName("value")] 21 | public Expression Value { private init; get; } 22 | [JsonPropertyName("tag")] 23 | public string Tag { private init; get; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Response/Answer.cs: -------------------------------------------------------------------------------- 1 | using TwitterSharp.Response.RMedia; 2 | using TwitterSharp.Response.RTweet; 3 | using TwitterSharp.Response.RUser; 4 | 5 | namespace TwitterSharp.Response 6 | { 7 | internal class Answer : BaseAnswer 8 | { 9 | public T Data { set; get; } 10 | } 11 | 12 | internal class BaseAnswer 13 | { 14 | public Meta Meta { init; get; } 15 | public Includes Includes { init; get; } 16 | public MatchingRule[] MatchingRules { init; get; } 17 | 18 | // Error Handling 19 | public string Detail { init; get; } 20 | public string Title { init; get; } 21 | public string Type { init; get; } 22 | public Error[] Errors { init; get; } 23 | } 24 | 25 | internal class Includes 26 | { 27 | public User[] Users { init; get; } 28 | public Media[] Media { init; get; } 29 | } 30 | 31 | internal class Meta 32 | { 33 | public Summary Summary { init; get; } 34 | public string NextToken { init; get; } 35 | } 36 | 37 | internal class Summary 38 | { 39 | public int Created { init; get; } 40 | public int NotCreated { init; get; } 41 | public int Valid { init; get; } 42 | public int Invalid { init; get; } 43 | public int Deleted { init; get; } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Response/Entity/AEntity.cs: -------------------------------------------------------------------------------- 1 | namespace TwitterSharp.Response.Entity 2 | { 3 | public abstract class AEntity 4 | { 5 | public int Start { init; get; } 6 | public int End { init; get; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Response/Entity/Entities.cs: -------------------------------------------------------------------------------- 1 | namespace TwitterSharp.Response.Entity 2 | { 3 | public class Entities 4 | { 5 | public EntityUrl[] Urls { set; get; } 6 | public EntityTag[] Hashtags { set; get; } 7 | public EntityTag[] Cashtags { set; get; } 8 | public EntityTag[] Mentions { set; get; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Response/Entity/EntityTag.cs: -------------------------------------------------------------------------------- 1 | namespace TwitterSharp.Response.Entity 2 | { 3 | public class EntityTag : AEntity 4 | { 5 | public string Tag { init; get; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/Response/Entity/EntityUrl.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace TwitterSharp.Response.Entity 4 | { 5 | public class EntityUrl : AEntity 6 | { 7 | public string Url { init; get; } 8 | [JsonPropertyName("expanded_url")] 9 | public string ExpandedUrl { init; get; } 10 | [JsonPropertyName("display_url")] 11 | public string DisplayUrl { init; get; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Response/Error.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace TwitterSharp.Response 4 | { 5 | public class Error 6 | { 7 | public string Parameter { init; get; } 8 | public dynamic Parameters { init; get; } 9 | public string[] Details { init; get; } 10 | public string Code { init; get; } 11 | public string Value { init; get; } 12 | public string Message { init; get; } 13 | public string Title { init; get; } 14 | public string Type { init; get; } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Response/RMedia/Media.cs: -------------------------------------------------------------------------------- 1 | namespace TwitterSharp.Response.RMedia 2 | { 3 | // MUST BE PARSED IN MEDIACONVERTER 4 | public class Media 5 | { 6 | public string Key { init; get; } 7 | public MediaType? Type { init; get; } 8 | public int? Height { init; get; } 9 | public int? Width { init; get; } 10 | public int? DurationMs { init; get; } 11 | public string PreviewImageUrl { init; get; } 12 | public string Url { init; get; } 13 | public MediaVariant[] Variants { init; get; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Response/RMedia/MediaType.cs: -------------------------------------------------------------------------------- 1 | namespace TwitterSharp.Response.RMedia 2 | { 3 | public enum MediaType 4 | { 5 | AnimatedGif, 6 | Photo, 7 | Video 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Response/RMedia/MediaVariant.cs: -------------------------------------------------------------------------------- 1 | namespace TwitterSharp.Response.RMedia 2 | { 3 | public class MediaVariant 4 | { 5 | public int? BitRate { init; get; } 6 | public string ContentType { init; get; } 7 | public string Url { init; get; } 8 | } 9 | } -------------------------------------------------------------------------------- /src/Response/RStream/StreamInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using TwitterSharp.Rule; 3 | 4 | namespace TwitterSharp.Response.RStream 5 | { 6 | public class StreamInfo : IEquatable 7 | { 8 | public string Id { init; get; } 9 | 10 | // TODO: I want to better handle this but there is too much parsing: https://developer.twitter.com/en/docs/twitter-api/tweets/search/integrate/build-a-query 11 | public Expression Value { init; get; } 12 | public string Tag { init; get; } 13 | 14 | public override bool Equals(object obj) 15 | => obj is StreamInfo t && t?.Id == Id; 16 | 17 | public bool Equals(StreamInfo other) 18 | => other?.Id == Id; 19 | 20 | public override int GetHashCode() 21 | => Id.GetHashCode(); 22 | 23 | public static bool operator ==(StreamInfo left, StreamInfo right) 24 | => left?.Id == right?.Id; 25 | 26 | public static bool operator !=(StreamInfo left, StreamInfo right) 27 | => left?.Id != right?.Id; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Response/RTweet/Attachments.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using TwitterSharp.Response.RMedia; 3 | 4 | namespace TwitterSharp.Response.RTweet 5 | { 6 | public class Attachments 7 | { 8 | //[JsonPropertyName("poll_ids")] 9 | //public Media[] Polls { init; get; } 10 | [JsonPropertyName("media_keys")] 11 | public Media[] Media { set; get; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Response/RTweet/EditControls.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace TwitterSharp.Response.RTweet 4 | { 5 | /// 6 | /// You can get information such as whether a Tweet was editable at the time it was created, how much time, 7 | /// if any, is remaining for a Tweet to be edited, and how many edits remain by specifying edit_controls in 8 | /// your tweet.fields parameter. 9 | /// 10 | public class EditControls 11 | { 12 | /// 13 | /// Tweet was editable at the time it was created 14 | /// 15 | public bool IsEditEligible { get; init; } 16 | /// 17 | /// How much time, if any, is remaining for a Tweet to be edited 18 | /// 19 | public DateTime EditableUntil { get; init; } 20 | /// 21 | /// How many edits remain 22 | /// 23 | public int EditsRemaining { get; init; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Response/RTweet/MatchingRule.cs: -------------------------------------------------------------------------------- 1 | namespace TwitterSharp.Response.RTweet 2 | { 3 | public record MatchingRule 4 | { 5 | public string Id { init; get; } 6 | public string Tag { init; get; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Response/RTweet/ReferenceType.cs: -------------------------------------------------------------------------------- 1 | namespace TwitterSharp.Response.RTweet 2 | { 3 | public enum ReferenceType 4 | { 5 | RepliedTo, 6 | Quoted, 7 | Retweeted 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Response/RTweet/ReferencedTweet.cs: -------------------------------------------------------------------------------- 1 | namespace TwitterSharp.Response.RTweet 2 | { 3 | public class ReferencedTweet 4 | { 5 | public ReferenceType Type { internal set; get; } 6 | public string Id { internal set; get; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Response/RTweet/ReplySettings.cs: -------------------------------------------------------------------------------- 1 | namespace TwitterSharp.Response.RTweet 2 | { 3 | public enum ReplySettings 4 | { 5 | Everyone, 6 | MentionnedUsers, 7 | Followers 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Response/RTweet/Tweet.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json.Serialization; 3 | using TwitterSharp.Client; 4 | using TwitterSharp.Model; 5 | using TwitterSharp.Response.Entity; 6 | using TwitterSharp.Response.RMedia; 7 | using TwitterSharp.Response.RUser; 8 | 9 | namespace TwitterSharp.Response.RTweet 10 | { 11 | public class Tweet : IEquatable, IHaveAuthor, IHaveMedia, IHaveMatchingRules 12 | { 13 | /// 14 | /// Unique identifier of the tweet 15 | /// 16 | public string Id { init; get; } 17 | /// 18 | /// Text of the tweet 19 | /// 20 | public string Text { init; get; } 21 | /// 22 | /// ID of the user who created the tweet 23 | /// 24 | public string AuthorId { init; get; } 25 | /// 26 | /// The ID of the tweet that the conversation is from 27 | /// 28 | public string ConversationId { init; get; } 29 | /// 30 | /// When the tweet was created 31 | /// 32 | public DateTime? CreatedAt { init; get; } 33 | /// 34 | /// Information about special entities in the tweet 35 | /// 36 | public Entities Entities { init; get; } 37 | /// 38 | /// ID of the user to whom the tweet is replying 39 | /// 40 | public string InReplyToUserId { init; get; } 41 | /// 42 | /// Language of the tweet (detected by Twitter) 43 | /// 44 | public string Lang { init; get; } 45 | /// 46 | /// If the tweet may contain sensitive information 47 | /// 48 | public bool? PossiblySensitive { init; get; } 49 | /// 50 | /// An array of Tweet IDs corresponding to each version of a Tweet, beginning with the initial Tweet and ending with the most recent version of the Tweet. 51 | /// 52 | public string[] EditHistoryTweetIds { init; get; } 53 | /// 54 | /// You can get information such as whether a Tweet was editable at the time it was created, how much time, 55 | /// if any, is remaining for a Tweet to be edited, and how many edits remain by specifying edit_controls 56 | /// in your tweet.fields parameter. 57 | /// 58 | public EditControls EditControls { get; set; } 59 | /// 60 | /// Public metrics of the tweet 61 | /// 62 | public TweetPublicMetrics PublicMetrics { init; get; } 63 | /// 64 | /// Tweet that were referenced by this one 65 | /// 66 | public ReferencedTweet[] ReferencedTweets { init; get; } 67 | /// 68 | /// Who can reply to this tweet 69 | /// 70 | public ReplySettings? ReplySettings { init; get; } 71 | /// 72 | /// Objects attached to the tweet 73 | /// 74 | public Attachments Attachments { set; get; } 75 | 76 | [JsonIgnore] 77 | public User Author { internal set; get; } 78 | 79 | // Interface 80 | void IHaveAuthor.SetAuthor(User author) 81 | => Author = author; 82 | [JsonIgnore] 83 | string IHaveAuthor.AuthorId => AuthorId; 84 | 85 | Media[] IHaveMedia.GetMedia() 86 | => Attachments?.Media; 87 | 88 | [JsonIgnore] 89 | public MatchingRule[] MatchingRules { set; get; } 90 | 91 | // Comparison 92 | 93 | public override bool Equals(object obj) 94 | => obj is Tweet t && t?.Id == Id; 95 | 96 | public bool Equals(Tweet other) 97 | => other?.Id == Id; 98 | 99 | public override int GetHashCode() 100 | => Id.GetHashCode(); 101 | 102 | public static bool operator ==(Tweet left, Tweet right) 103 | => left?.Id == right?.Id; 104 | 105 | public static bool operator !=(Tweet left, Tweet right) 106 | => left?.Id != right?.Id; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Response/RTweet/TweetPublicMetrics.cs: -------------------------------------------------------------------------------- 1 | namespace TwitterSharp.Response.RTweet 2 | { 3 | public class TweetPublicMetrics 4 | { 5 | public int RetweetCount { init; get; } 6 | public int ReplyCount { init; get; } 7 | public int LikeCount { init; get; } 8 | public int QuoteCount { init; get; } 9 | public int ImpressionCount { init; get; } 10 | } 11 | } -------------------------------------------------------------------------------- /src/Response/RUser/RUsers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace TwitterSharp.Response.RUser 5 | { 6 | public class RUsers 7 | { 8 | public User[] Users { init; get; } 9 | public Func> NextAsync { init; get; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Response/RUser/User.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using TwitterSharp.Response.Entity; 3 | 4 | namespace TwitterSharp.Response.RUser 5 | { 6 | public class User : IEquatable 7 | { 8 | /// 9 | /// Unique identifier of the user 10 | /// 11 | public string Id { init; get; } 12 | /// 13 | /// Name shown on the user profile 14 | /// 15 | public string Name { init; get; } 16 | /// 17 | /// Twitter handle 18 | /// 19 | public string Username { init; get; } 20 | 21 | // OPTIONAL 22 | 23 | /// 24 | /// Creation date of the account 25 | /// 26 | public DateTime? CreatedAt { init; get; } 27 | /// 28 | /// Profile description 29 | /// 30 | public string Description { init; get; } 31 | /// 32 | /// Location specified on user profile 33 | /// 34 | public string Location { init; get; } 35 | /// 36 | /// Identifier of the pinned tweet 37 | /// 38 | public string PinnedTweetId { init; get; } 39 | /// 40 | /// URL to the picture profile 41 | /// 42 | public string ProfileImageUrl { init; get; } 43 | /// 44 | /// Are the user tweet private 45 | /// 46 | public bool? Protected { init; get; } 47 | /// 48 | /// URL specified on user profile 49 | /// 50 | public string Url { init; get; } 51 | /// 52 | /// Information about special entities in user description 53 | /// 54 | public Entities Entities { init; get; } 55 | /// 56 | /// Public metrics of the user 57 | /// 58 | public UserPublicMetrics PublicMetrics { init; get; } 59 | /// 60 | /// If the user is a verified Twitter user 61 | /// 62 | public bool? Verified { init; get; } 63 | 64 | /// 65 | /// indicates the type of verification a user account has: blue, business, government or none 66 | /// 67 | public string VerifiedType { init; get; } 68 | 69 | public override bool Equals(object obj) 70 | => obj is User t && t?.Id == Id; 71 | 72 | public bool Equals(User other) 73 | => other?.Id == Id; 74 | 75 | public override int GetHashCode() 76 | => Id.GetHashCode(); 77 | 78 | public static bool operator ==(User left, User right) 79 | => left?.Id == right?.Id; 80 | 81 | public static bool operator !=(User left, User right) 82 | => left?.Id != right?.Id; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Response/RUser/UserPublicMetrics.cs: -------------------------------------------------------------------------------- 1 | namespace TwitterSharp.Response.RUser 2 | { 3 | public class UserPublicMetrics 4 | { 5 | public int FollowersCount { init; get; } 6 | public int FollowingCount { init; get; } 7 | public int TweetCount { init; get; } 8 | public int ListedCount { init; get; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Response/RateLimit.cs: -------------------------------------------------------------------------------- 1 | using TwitterSharp.ApiEndpoint; 2 | 3 | namespace TwitterSharp.Response 4 | { 5 | public class RateLimit 6 | { 7 | public Endpoint Endpoint { get; set; } 8 | public int Limit { get; internal set; } 9 | public int Remaining { get; internal set; } 10 | public int Reset { get; internal set; } 11 | 12 | public RateLimit(Endpoint endpoint) 13 | { 14 | Endpoint = endpoint; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Rule/Expression.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System; 3 | using System.Linq; 4 | using System.Text.RegularExpressions; 5 | 6 | namespace TwitterSharp.Rule 7 | { 8 | // https://developer.twitter.com/en/docs/twitter-api/tweets/filtered-stream/integrate/build-a-rule 9 | public class Expression 10 | { 11 | internal Expression(string prefix, string userInput) 12 | { 13 | _internal = prefix + (userInput != null ? (userInput.Contains(' ') && !userInput.StartsWith('\"') && !userInput.EndsWith('\"') ? "\"" + userInput + "\"" : userInput) : ""); 14 | } 15 | 16 | /// 17 | /// Constructor for building single expressions 18 | /// 19 | /// Prefix like #, @, from: or is:retweet 20 | /// Value of the expression 21 | /// Type of the expression 22 | private Expression(string prefix, string userInput, ExpressionType type) : this(prefix, userInput) 23 | { 24 | Type = type; 25 | } 26 | 27 | /// 28 | /// Constructor for building expression tree 29 | /// 30 | /// Prefix like #, @, from: or is:retweet 31 | /// Value of the expression 32 | /// Type of the expression 33 | /// first expression from And/Or logic 34 | /// grouped expressions 35 | private Expression(string prefix, string userInput, ExpressionType type, Expression firstExpression, Expression[] expressions) : this(prefix, userInput, type) 36 | { 37 | if (firstExpression != null && expressions is { Length: > 0 }) 38 | { 39 | Expressions = new Expression[expressions.Length + 1]; 40 | Expressions[0] = firstExpression; 41 | expressions.CopyTo(Expressions, 1); 42 | } 43 | else if (firstExpression != null) 44 | { 45 | Expressions = new [] { firstExpression }; 46 | } 47 | } 48 | 49 | public ExpressionType Type { get; } 50 | public Expression[] Expressions { get; } 51 | public bool IsNegate { get; private set; } 52 | 53 | private string _internal; 54 | 55 | public override string ToString() 56 | => _internal; 57 | 58 | /// 59 | /// Parses the given string into an expression with type and expression tree 60 | /// 61 | /// Expression string following the rules from Building rules for filtered stream 62 | /// Expression with type and expression tree 63 | public static Expression Parse(string s) 64 | { 65 | const char r = '~'; // replaceCharacter 66 | 67 | // Find Quote and Replace 68 | // https://regex101.com/r/9P1hCA/1 69 | var replacements = new List(); 70 | var replacementCount = 0; 71 | 72 | foreach (Match match in Regex.Matches(s, "(\\\").*?(\")")) 73 | { 74 | replacements.Add(match.Value); 75 | s = ReplaceFirst(s, match.Value, $"{r}q{replacementCount++}{r}"); 76 | } 77 | 78 | // Find coordinates and Replace 79 | foreach (Match match in Regex.Matches(s, @"\[.*?\]")) 80 | { 81 | replacements.Add(match.Value); 82 | s = ReplaceFirst(s, match.Value, $"{r}q{replacementCount++}{r}"); 83 | } 84 | 85 | var expressions = new List(); 86 | var expressionCount = 0; 87 | 88 | // Finding most bottom (single) expressions 89 | foreach (var stringExpression in s.Replace($" OR ", " ").Replace("-(", "").Replace("(", "").Replace(")", "").Split(' ')) 90 | { 91 | var isNegate = stringExpression.StartsWith('-') && !stringExpression.Equals("-is:nullcast", StringComparison.InvariantCultureIgnoreCase); 92 | var sr = isNegate ? stringExpression[1..] : stringExpression; 93 | 94 | if (stringExpression.Contains($"{r}q")) 95 | sr = Regex.Replace(sr, @$"{r}q(\d+){r}", replacements[int.Parse(Regex.Match(stringExpression, @"\d+").Value)]); 96 | 97 | if (sr.StartsWith('#')) 98 | AddExpression(Hashtag(sr[1..])); 99 | else if (sr.StartsWith('$')) 100 | AddExpression(Cashtag(sr[1..])); 101 | else if (sr.StartsWith('@')) 102 | AddExpression(Mention(sr[1..])); 103 | else if (sr.StartsWith("from:")) 104 | AddExpression(Author(sr.Replace("from:", ""))); 105 | else if (sr.StartsWith("to:")) 106 | AddExpression(Recipient(sr.Replace("to:", ""))); 107 | else if (sr.StartsWith("url:")) 108 | AddExpression(Url(sr.Replace("url:", ""))); 109 | else if (sr.StartsWith("retweets_of:")) 110 | AddExpression(Retweet(sr.Replace("retweets_of:", ""))); 111 | else if (sr.StartsWith("retweets_of_user:")) // Alias 112 | AddExpression(Retweet(sr.Replace("retweets_of:", ""))); 113 | else if (sr.StartsWith("context:")) 114 | AddExpression(Context(sr.Replace("context:", ""))); 115 | else if (sr.StartsWith("entity:")) 116 | AddExpression(Entity(sr.Replace("entity:", ""))); 117 | else if (sr.StartsWith("conversation_id:")) 118 | AddExpression(ConversationId(sr.Replace("conversation_id:", ""))); 119 | else if (sr.StartsWith("bio:")) 120 | AddExpression(Bio(sr.Replace("bio:", ""))); 121 | else if (sr.StartsWith("user_bio:")) // Alias 122 | AddExpression(Bio(sr.Replace("bio:", ""))); 123 | else if (sr.StartsWith("bio_location:")) 124 | AddExpression(BioLocation(sr.Replace("bio_location:", ""))); 125 | else if (sr.StartsWith("user_bio_location:")) // Alias 126 | AddExpression(BioLocation(sr.Replace("bio_location:", ""))); 127 | else if (sr.StartsWith("place:")) 128 | AddExpression(Place(sr.Replace("place:", ""))); 129 | else if (sr.StartsWith("place_country:")) 130 | AddExpression(PlaceCountry(sr.Replace("place_country:", ""))); 131 | else if (sr.StartsWith("point_radius:")) 132 | { 133 | var coordinates = sr.Replace("point_radius:", "").Replace("[", "").Replace("]", "").Split(' '); 134 | var unit = coordinates[2].EndsWith("km") ? RadiusUnit.Kilometer : RadiusUnit.Mile; 135 | AddExpression(PointRadius(coordinates[0], coordinates[1], coordinates[2][..^2], unit)); 136 | } 137 | else if (sr.StartsWith("bounding_box:")) 138 | { 139 | var coordinates = sr.Replace("bounding_box:", "").Replace("[", "").Replace("]", "").Split(' '); 140 | AddExpression(BoundingBox(coordinates[0], coordinates[1], coordinates[2], coordinates[3])); 141 | } 142 | else if (sr.StartsWith("geo_bounding_box:")) // Alias 143 | { 144 | var coordinates = sr.Replace("geo_bounding_box:", "").Replace("[", "").Replace("]", "").Split(' '); 145 | AddExpression(BoundingBox(coordinates[0], coordinates[1], coordinates[2], coordinates[3])); 146 | } 147 | else if (sr.StartsWith("sample:")) 148 | AddExpression(Sample(int.Parse(sr.Replace("sample:", "")))); 149 | else if (sr.StartsWith("lang:")) 150 | AddExpression(Lang(sr.Replace("lang:", ""))); 151 | else if (sr == "is:retweet") 152 | AddExpression(IsRetweet()); 153 | else if (sr == "is:reply") 154 | AddExpression(IsReply()); 155 | else if (sr == "is:quote") 156 | AddExpression(IsQuote()); 157 | else if (sr == "is:verified") 158 | AddExpression(IsVerified()); 159 | else if (sr == "-is:nullcast") 160 | AddExpression(IsNotNullcast()); 161 | else if (sr == "has:hashtags") 162 | AddExpression(HasHashtags()); 163 | else if (sr == "has:cashtags") 164 | AddExpression(HasCashtags()); 165 | else if (sr == "has:links") 166 | AddExpression(HasLinks()); 167 | else if (sr == "has:mentions") 168 | AddExpression(HasMentions()); 169 | else if (sr == "has:media") 170 | AddExpression(HasMedia()); 171 | else if (sr == "has:media_link") // Alias 172 | AddExpression(HasMedia()); 173 | else if (sr == "has:images") 174 | AddExpression(HasImages()); 175 | else if (sr == "has:videos") 176 | AddExpression(HasVideos()); 177 | else if (sr == "has:video_link") // Alias 178 | AddExpression(HasVideos()); 179 | else if (sr == "has:geo") 180 | AddExpression(HasGeo()); 181 | else if (sr.StartsWith("followers_count:")) 182 | AddCountExpression(sr, "followers_count:"); 183 | else if (sr.StartsWith("tweets_count:")) 184 | AddCountExpression(sr, "tweets_count:"); 185 | else if (sr.StartsWith("statuses_count:")) // Alias 186 | AddCountExpression(sr, "statuses_count:"); 187 | else if (sr.StartsWith("following_count:")) 188 | AddCountExpression(sr, "following_count:"); 189 | else if (sr.StartsWith("friends_count:")) // Alias 190 | AddCountExpression(sr, "friends_count:"); 191 | else if (sr.StartsWith("listed_count:")) 192 | AddCountExpression(sr, "listed_count:"); 193 | else if (sr.StartsWith("user_in_lists_count:")) // Alias 194 | AddCountExpression(sr, "user_in_lists_count:"); 195 | else if (sr.StartsWith("url_title:")) 196 | AddExpression(UrlTitle(sr.Replace("url_title:", ""))); 197 | else if (sr.StartsWith("within_url_title:")) // Alias 198 | AddExpression(UrlTitle(sr.Replace("within_url_title:", ""))); 199 | else if (sr.StartsWith("url_description:")) 200 | AddExpression(UrlDescription(sr.Replace("url_description:", ""))); 201 | else if (sr.StartsWith("within_url_description:")) // Alias 202 | AddExpression(UrlDescription(sr.Replace("within_url_description:", ""))); 203 | else if (sr.StartsWith("url_contains:")) 204 | AddExpression(UrlContains(sr.Replace("url_contains:", ""))); 205 | else if (sr.StartsWith("in_reply_to_tweet_id:")) 206 | AddExpression(InReplyToTweetId(long.Parse(sr.Replace("in_reply_to_tweet_id:", "")))); 207 | else if (sr.StartsWith("in_reply_to_status_id:")) 208 | AddExpression(InReplyToTweetId(long.Parse(sr.Replace("in_reply_to_status_id:", "")))); 209 | else if (sr.StartsWith("retweets_of_tweet_id:")) 210 | AddExpression(RetweetsOfTweetId(long.Parse(sr.Replace("retweets_of_tweet_id:", "")))); 211 | else if (sr.StartsWith("retweets_of_status_id:")) 212 | AddExpression(RetweetsOfTweetId(long.Parse(sr.Replace("retweets_of_status_id:", "")))); 213 | else 214 | AddExpression(Keyword(sr)); 215 | 216 | void AddCountExpression(string countExpressionString, string searchString) 217 | { 218 | if (countExpressionString.Contains("..")) 219 | { 220 | var matches = Regex.Matches(countExpressionString, @"\d+"); 221 | 222 | switch (searchString) 223 | { 224 | case "followers_count:": AddExpression(FollowersCount(int.Parse(matches[0].Value), int.Parse(matches[1].Value))); break; 225 | case "tweets_count:": AddExpression(TweetsCount(int.Parse(matches[0].Value), int.Parse(matches[1].Value))); break; 226 | case "statuses_count:": AddExpression(TweetsCount(int.Parse(matches[0].Value), int.Parse(matches[1].Value))); break; 227 | case "following_count:": AddExpression(FollowingCount(int.Parse(matches[0].Value), int.Parse(matches[1].Value))); break; 228 | case "friends_count:": AddExpression(FollowingCount(int.Parse(matches[0].Value), int.Parse(matches[1].Value))); break; 229 | case "listed_count:": AddExpression(ListedCount(int.Parse(matches[0].Value), int.Parse(matches[1].Value))); break; 230 | case "user_in_lists_count:": AddExpression(ListedCount(int.Parse(matches[0].Value), int.Parse(matches[1].Value))); break; 231 | } 232 | } 233 | else if (countExpressionString.StartsWith(searchString)) 234 | { 235 | var count = int.Parse(countExpressionString.Replace(searchString, "")); 236 | 237 | switch (searchString) 238 | { 239 | case "followers_count:": AddExpression(FollowersCount(count)); break; 240 | case "tweets_count:": AddExpression(TweetsCount(count)); break; 241 | case "statuses_count:": AddExpression(TweetsCount(count)); break; 242 | case "following_count:": AddExpression(FollowingCount(count)); break; 243 | case "friends_count:": AddExpression(FollowingCount(count)); break; 244 | case "listed_count:": AddExpression(ListedCount(count)); break; 245 | case "user_in_lists_count:": AddExpression(ListedCount(count)); break; 246 | } 247 | } 248 | } 249 | 250 | void AddExpression(Expression exp) 251 | { 252 | expressions.Add(isNegate ? exp.Negate() : exp); 253 | } 254 | 255 | s = ReplaceFirst(s, stringExpression, $"{r}e{expressionCount++}{r}"); 256 | } 257 | 258 | string ReplaceFirst(string text, string search, string replace) 259 | { 260 | var pos = text.IndexOf(search, StringComparison.Ordinal); 261 | if (pos < 0) 262 | { 263 | return text; 264 | } 265 | return text[..pos] + replace + text[(pos + search.Length)..]; 266 | } 267 | 268 | // Find groups recursive 269 | // https://regex101.com/r/xJaODO/2 270 | var groups = new Dictionary(); 271 | 272 | FindGroups(); 273 | 274 | void FindGroups() 275 | { 276 | // https://regex101.com/r/ONVk50/2 277 | foreach (Match match in Regex.Matches(s, "\\([^\\(]*?\\)")) 278 | { 279 | var group = match.Value.Replace("(", "").Replace(")", ""); 280 | AddToGroup(group); 281 | s = ReplaceFirst(s, $"({group})", $"{r}g{expressionCount++}{r}"); 282 | } 283 | 284 | if (s.Contains('(')) 285 | { 286 | FindGroups(); 287 | } 288 | else if (s.Contains(" OR ") && s.Contains($"{r} {r}")) // Mixed groups 289 | { 290 | var ors = s.Split(" OR "); 291 | 292 | for (int i = 0; i <= ors.Length-1; i++) 293 | { 294 | if (ors[i].Contains(' ')) 295 | { 296 | ors[i] = $"({ors[i]})"; 297 | } 298 | } 299 | 300 | s = string.Join(" OR ", ors); 301 | FindGroups(); 302 | } 303 | else 304 | { 305 | AddToGroup(s); // most top group should be left 306 | } 307 | 308 | void AddToGroup(string ga) 309 | { 310 | if (ga.Contains(" OR ")) 311 | { 312 | groups.Add(ga.Split(" OR "), false); 313 | } 314 | else 315 | { 316 | groups.Add(ga.Split(" "), true); 317 | } 318 | } 319 | } 320 | 321 | // build expression 322 | foreach (var group in groups) 323 | { 324 | var groupExpression = new List(); 325 | 326 | foreach (var k in group.Key) 327 | { 328 | var index = Regex.Match(k, @"\d+").Value; 329 | groupExpression.Add(expressions[int.Parse(index)]); 330 | 331 | if (k.StartsWith('-')) 332 | { 333 | expressions[int.Parse(index)].Negate(); 334 | } 335 | } 336 | 337 | if (groupExpression.Count > 1) 338 | { 339 | expressions.Add(group.Value // true is And / false is Or 340 | ? groupExpression[0].And(groupExpression.Skip(1).ToArray()) 341 | : groupExpression[0].Or(groupExpression.Skip(1).ToArray())); 342 | } 343 | } 344 | 345 | return expressions.Last(); 346 | } 347 | 348 | // LOGIC 349 | 350 | /// 351 | /// Tweet match one of the expression given in parameter 352 | /// 353 | public Expression Or(params Expression[] others) 354 | { 355 | return new(others.Any() ? "(" + _internal + " OR " + string.Join(" OR ", others.Select(x => x.ToString())) + ")" : _internal, "", ExpressionType.Or, this, others); 356 | } 357 | 358 | /// 359 | /// Tweet match all the expressions given in parameter 360 | /// 361 | public Expression And(params Expression[] others) 362 | { 363 | return new(others.Any() ? "(" + _internal + " " + string.Join(" ", others.Select(x => x.ToString())) + ")" : _internal, "", ExpressionType.And, this, others); 364 | } 365 | 366 | /// 367 | /// Tweet match the negation of the current expression 368 | /// 369 | public Expression Negate() 370 | { 371 | if (!_internal.StartsWith('-')) // prevent double negation 372 | _internal = "-" + _internal; 373 | 374 | IsNegate = true; 375 | return this; 376 | } 377 | 378 | // OPERATORS 379 | 380 | /// 381 | /// Match a keyword in the body of a tweet 382 | /// 383 | public static Expression Keyword(string str) 384 | => new("", str, ExpressionType.Keyword); 385 | 386 | /// 387 | /// Any tweet with the given hashtag 388 | /// 389 | public static Expression Hashtag(string entity) 390 | => new("#", entity, ExpressionType.Hashtag); 391 | 392 | /// 393 | /// Any tweet with the given cashtag 394 | /// 395 | public static Expression Cashtag(string entity) 396 | => new("$", entity, ExpressionType.Cashtag); 397 | 398 | /// 399 | /// Any tweet that contains a mention of the given user 400 | /// 401 | public static Expression Mention(string username) 402 | => new("@", username, ExpressionType.Mention); 403 | 404 | /// 405 | /// Any tweet sent from a specific user 406 | /// 407 | public static Expression Author(string username) 408 | => new("from:", username, ExpressionType.Author); 409 | 410 | /// 411 | /// Any tweet that is in reply to a specific user 412 | /// 413 | public static Expression Recipient(string username) 414 | => new("to:", username, ExpressionType.Recipient); 415 | 416 | /// 417 | /// Match a valid tweeter URL 418 | /// 419 | public static Expression Url(string twitterUrl) 420 | => new("url:", twitterUrl, ExpressionType.Url); 421 | 422 | /// 423 | /// Match tweet that are a retweet of a specific user 424 | /// 425 | public static Expression Retweet(string username) 426 | => new("retweets_of:", username, ExpressionType.Retweet); 427 | 428 | /// 429 | /// https://developer.twitter.com/en/docs/twitter-api/annotations 430 | /// 431 | public static Expression Context(string str) 432 | => new("context:", str, ExpressionType.Context); 433 | 434 | /// 435 | /// Match an entity (parameter is the string declaration of entity/place) 436 | /// 437 | public static Expression Entity(string str) 438 | => new("entity:", str, ExpressionType.Entity); 439 | 440 | /// 441 | /// Match tweet with a specific conversation ID 442 | /// A conversation ID is the ID of a tweet that started a conversation 443 | /// 444 | public static Expression ConversationId(string id) 445 | => new("conversation_id:", id, ExpressionType.ConversationId); 446 | 447 | /// 448 | /// Match a keyword within the tweet publisher's user bio name 449 | /// 450 | public static Expression Bio(string keyword) 451 | => new("bio:", keyword, ExpressionType.Bio); 452 | 453 | /// 454 | /// Match tweet that are published by users whose location contains a specific keyword 455 | /// 456 | /// 457 | /// 458 | public static Expression BioLocation(string location) 459 | => new("bio_location:", location, ExpressionType.BioLocation); 460 | 461 | /// 462 | /// Tweets tagged with a specific location 463 | /// 464 | public static Expression Place(string location) 465 | => new("place:", location, ExpressionType.Place); 466 | 467 | /// 468 | /// Tweets tagged with a specific country 469 | /// 470 | /// Must be a valid ISO 3166-1 alpha-2 code 471 | public static Expression PlaceCountry(string country) 472 | => new("place_country:", country, ExpressionType.PlaceCountry); 473 | 474 | /// 475 | /// Match against the geocoordinate of a tweet 476 | /// 477 | /// Must be in decimal degree, is in range of ±180 478 | /// Must be in decimal degree, is in range of ±90 479 | /// Must be less than 25 miles 480 | /// km or mi 481 | public static Expression PointRadius(string longitude, string latitude, string radius, RadiusUnit radiusUnit) 482 | => new($"point_radius:[{longitude} {latitude} {radius}{(radiusUnit == RadiusUnit.Kilometer ? "km" : "mi")}]", "", ExpressionType.PointRadius); 483 | 484 | /// 485 | /// Width and height of the box must be less than 25 miles 486 | /// 487 | /// Longitude of the southwest corner, is in range of ±180, decimal degree 488 | /// Latitude of the southwest corner, is in range of ±90, decimal degree 489 | /// Longitude of the northeast corner, is in range of ±180, decimal degree 490 | /// Latitude of the northeast corner, is in range of ±90, decimal degree 491 | public static Expression BoundingBox(string westLongitude, string southLatitude, string eastLongitude, string northLatitude) 492 | => new($"bounding_box:[{westLongitude} {southLatitude} {eastLongitude} {northLatitude}]", "", ExpressionType.BoundingBox); 493 | 494 | /// 495 | /// Match retweets (doesn't include quote tweets) 496 | /// 497 | public static Expression IsRetweet() 498 | => new("is:retweet", "", ExpressionType.IsRetweet); 499 | 500 | /// 501 | /// Match replies 502 | /// 503 | public static Expression IsReply() 504 | => new("is:reply", "", ExpressionType.IsReply); 505 | 506 | /// 507 | /// Match quote tweets 508 | /// 509 | public static Expression IsQuote() 510 | => new("is:quote", "", ExpressionType.IsQuote); 511 | 512 | /// 513 | /// Only match tweets from verified accounts 514 | /// 515 | public static Expression IsVerified() 516 | => new("is:verified", "", ExpressionType.IsVerified); 517 | 518 | /// 519 | /// Remove tweets created for promotion only on ads.twitter.com 520 | /// Can't be negated 521 | /// 522 | public static Expression IsNotNullcast() 523 | => new("-is:nullcast", "", ExpressionType.IsNotNullcast); 524 | 525 | /// 526 | /// Only match tweets that contains at least one hashtag 527 | /// 528 | public static Expression HasHashtags() 529 | => new("has:hashtags", "", ExpressionType.HasHashtags); 530 | 531 | /// 532 | /// Only match tweets that contains at least one cashtag 533 | /// 534 | public static Expression HasCashtags() 535 | => new("has:cashtags", "", ExpressionType.HasCashtags); 536 | 537 | /// 538 | /// Only match tweets that contains at least one link/media in their body 539 | /// 540 | public static Expression HasLinks() 541 | => new("has:links", "", ExpressionType.HasLinks); 542 | 543 | /// 544 | /// Only match tweets that mention another user 545 | /// 546 | public static Expression HasMentions() 547 | => new("has:mentions", "", ExpressionType.HasMentions); 548 | 549 | /// 550 | /// Only match tweets that contains at least one media (photo/GIF/video) 551 | /// Doesn't match media created with Periscope or tweets that link to other media hosting sites 552 | /// 553 | public static Expression HasMedia() 554 | => new("has:media", "", ExpressionType.HasMedia); 555 | 556 | /// 557 | /// Only match tweets that contains an URL to an image 558 | /// 559 | public static Expression HasImages() 560 | => new("has:images", "", ExpressionType.HasImages); 561 | 562 | /// 563 | /// Only match tweets that contains a video uploaded to 564 | /// Doesn't match media created with Periscope or tweets that link to other media hosting sites 565 | /// 566 | public static Expression HasVideos() 567 | => new("has:videos", "", ExpressionType.HasVideos); 568 | 569 | /// 570 | /// Only match tweet that contains geolocation data 571 | /// 572 | public static Expression HasGeo() 573 | => new("has:geo", "", ExpressionType.HasGeo); 574 | 575 | /// 576 | /// Only returns a percentage of tweet that match a rule 577 | /// 578 | public static Expression Sample(int percent) 579 | => new("sample:" + percent, "", ExpressionType.Sample); 580 | 581 | /// 582 | /// Match tweets that has been classified as being of a specific language 583 | /// A tweet can only be of one language 584 | /// 585 | /// Must be a valid BCP 47 code 586 | public static Expression Lang(string countryCode) 587 | => new("lang:", countryCode, ExpressionType.Lang); 588 | 589 | /// 590 | /// Matches Tweets when the author has a followers count within the given range. 591 | /// Example: followers_count:500 592 | /// 593 | /// Any number equal to or higher will match 594 | public static Expression FollowersCount(int count) 595 | => new("followers_count:", count.ToString(), ExpressionType.FollowersCount); 596 | 597 | /// 598 | /// Matches Tweets when the author has a followers count within the given range. 599 | /// Example: followers_count:1000..10000 600 | /// 601 | public static Expression FollowersCount(int from, int to) 602 | => new("followers_count:", from + ".." + to, ExpressionType.FollowersCount); 603 | 604 | /// 605 | /// Matches Tweets when the author has posted a number of Tweets that falls within the given range. 606 | /// Example: tweets_count:500 607 | /// 608 | /// Any number equal to or higher will match 609 | public static Expression TweetsCount(int count) 610 | => new("tweets_count:", count.ToString(), ExpressionType.TweetsCount); 611 | 612 | /// 613 | /// Matches Tweets when the author has posted a number of Tweets that falls within the given range. 614 | /// Example: tweets_count:1000..10000 615 | /// 616 | public static Expression TweetsCount(int from, int to) 617 | => new("tweets_count:", from + ".." + to, ExpressionType.TweetsCount); 618 | 619 | /// 620 | /// Matches Tweets when the author has a friends count (the number of users they follow) that falls within the given range. 621 | /// Example: following_count:500 622 | /// 623 | /// Any number equal to or higher will match 624 | public static Expression FollowingCount(int count) 625 | => new("following_count:", count.ToString(), ExpressionType.FollowingCount); 626 | 627 | /// 628 | /// Matches Tweets when the author has a friends count (the number of users they follow) that falls within the given range. 629 | /// Example: following_count:1000..10000 630 | /// 631 | public static Expression FollowingCount(int from, int to) 632 | => new("following_count:", from + ".." + to, ExpressionType.FollowingCount); 633 | 634 | /// 635 | /// Matches Tweets when the author is included in the specified number of Lists. 636 | /// Example: listed_count:500 637 | /// 638 | /// Any number equal to or higher will match 639 | public static Expression ListedCount(int count) 640 | => new("listed_count:", count.ToString(), ExpressionType.ListedCount); 641 | 642 | /// 643 | /// Matches Tweets when the author is included in the specified number of Lists. 644 | /// Example: listed_count:1000..10000 645 | /// 646 | public static Expression ListedCount(int from, int to) 647 | => new("listed_count:", from + ".." + to, ExpressionType.ListedCount); 648 | 649 | /// 650 | /// Performs a keyword/phrase match on the expanded URL HTML title metadata. 651 | /// Example: url_title:snow 652 | /// 653 | public static Expression UrlTitle(string title) 654 | => new("url_title:", title, ExpressionType.UrlTitle); 655 | 656 | /// 657 | /// Performs a keyword/phrase match on the expanded page description metadata. 658 | /// Example: url_description:weather 659 | /// 660 | public static Expression UrlDescription(string description) 661 | => new("url_description:", description, ExpressionType.UrlDescription); 662 | 663 | /// 664 | /// Matches Tweets with URLs that literally contain the given phrase or keyword. To search for patterns with punctuation in them (i.e. google.com) enclose the search term in quotes. 665 | /// NOTE: This will match against the expanded URL as well. 666 | /// Example: url_contains:photos 667 | /// 668 | public static Expression UrlContains(string contains) 669 | => new("url_contains:", contains, ExpressionType.UrlContains); 670 | 671 | /// 672 | /// Deliver only explicit Replies to the specified Tweet. 673 | /// Example: in_reply_to_tweet_id:1539382664746020864 674 | /// 675 | public static Expression InReplyToTweetId(long tweetId) 676 | => new("in_reply_to_tweet_id:", tweetId.ToString(), ExpressionType.InReplyToTweetId); 677 | 678 | /// 679 | /// Deliver only explicit (or native) Retweets of the specified Tweet. Note that the status ID used should be the ID of an original Tweet and not a Retweet. 680 | /// Example: retweets_of_tweet_id:1539382664746020864 681 | /// 682 | public static Expression RetweetsOfTweetId(long tweetId) 683 | => new("retweets_of_tweet_id:", tweetId.ToString(), ExpressionType.RetweetsOfTweetId); 684 | } 685 | } 686 | -------------------------------------------------------------------------------- /src/Rule/ExpressionType.cs: -------------------------------------------------------------------------------- 1 | namespace TwitterSharp.Rule 2 | { 3 | public enum ExpressionType 4 | { 5 | Or, And, Keyword, Hashtag, Cashtag, Mention, Author, Recipient, Url, Retweet, Context, Entity, 6 | ConversationId, Bio, BioLocation, Place, PlaceCountry, PointRadius, BoundingBox, IsRetweet, IsReply, IsQuote, 7 | IsVerified, IsNotNullcast, HasHashtags, HasCashtags, HasLinks, HasMentions, HasMedia, HasImages, HasVideos, 8 | HasGeo, Sample, Lang, FollowersCount, TweetsCount, FollowingCount, ListedCount, UrlTitle, UrlDescription, 9 | UrlContains, InReplyToTweetId, RetweetsOfTweetId 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Rule/RadiusUnit.cs: -------------------------------------------------------------------------------- 1 | namespace TwitterSharp.Rule 2 | { 3 | public enum RadiusUnit 4 | { 5 | Mile, 6 | Kilometer 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/TwitterSharp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net5.0;net6.0;net7.0 5 | true 6 | 2.4.0 7 | Xwilarg 8 | C# wrapper around Twitter API V2 9 | MIT 10 | LICENSE 11 | https://github.com/Xwilarg/TwitterSharp 12 | https://github.com/Xwilarg/TwitterSharp 13 | twitter-api twitter-api-v2 14 | 15 | 16 | 17 | 18 | True 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /test/TestExpression.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using TwitterSharp.Rule; 4 | 5 | namespace TwitterSharp.UnitTests 6 | { 7 | [TestClass] 8 | public class TestExpression 9 | { 10 | [TestMethod] 11 | public void TestOr() 12 | { 13 | var exp = Expression.Author("achan_UGA").Or(Expression.Author("tanigox"), Expression.Author("daidoushinove")); 14 | Assert.AreEqual("(from:achan_UGA OR from:tanigox OR from:daidoushinove)", exp.ToString()); 15 | } 16 | 17 | [TestMethod] 18 | public void TestAnd() 19 | { 20 | var exp = Expression.Keyword("Test Keyword").And(Expression.IsReply().Negate(), Expression.IsRetweet().Negate()); 21 | Assert.AreEqual("(\"Test Keyword\" -is:reply -is:retweet)", exp.ToString()); 22 | } 23 | 24 | [TestMethod] 25 | public void NegateIsNotNullcast() 26 | { 27 | var exp = Expression.Keyword("foo").And(Expression.IsNotNullcast().Negate()); 28 | Assert.AreEqual("(foo -is:nullcast)", exp.ToString()); 29 | } 30 | 31 | [TestMethod] 32 | public void PreventDoubleNegate() 33 | { 34 | var exp = Expression.Keyword("foo").And(Expression.Keyword("bar").Negate().Negate().Negate()); 35 | Assert.AreEqual("(foo -bar)", exp.ToString()); 36 | } 37 | 38 | [TestMethod] 39 | public void EmptyGroup() 40 | { 41 | var exp = Expression.Keyword("foo").Or(Array.Empty()); 42 | Assert.AreEqual("foo", exp.ToString()); 43 | } 44 | 45 | [TestMethod] 46 | public void FindExpressionsByType() 47 | { 48 | var exp1 = Expression.Keyword("Twitter API"); 49 | var exp2 = Expression.Hashtag("v2"); 50 | var expA = exp1.Or(exp2); 51 | var exp3 = Expression.Keyword("recent search"); 52 | var exp3B = exp3.Negate(); 53 | var expB = expA.And(exp3B); 54 | var exp4 = Expression.Keyword("grumpy"); 55 | var exp5 = Expression.Keyword("cat"); 56 | var expC = exp4.And(exp5); 57 | var exp6 = Expression.Hashtag("meme"); 58 | var exp7 = Expression.HasImages(); 59 | var exp8 = Expression.IsRetweet(); 60 | var exp8B = exp8.Negate(); 61 | var expD = exp6.And(exp7, exp8B); 62 | var expE = expC.Or(expD); 63 | var expF = expB.Or(expE); 64 | 65 | int CountExpressionsOfType(Expression expression, ExpressionType type) 66 | { 67 | var i = 0; 68 | 69 | if (expression.Type == type) 70 | { 71 | i++; 72 | } 73 | 74 | if (expression.Expressions != null) 75 | { 76 | foreach (var exp in expression.Expressions) 77 | { 78 | i += CountExpressionsOfType(exp, type); 79 | } 80 | } 81 | 82 | return i; 83 | } 84 | 85 | Assert.AreEqual(CountExpressionsOfType(expF, ExpressionType.Hashtag), 2); 86 | Assert.AreEqual(CountExpressionsOfType(expF, ExpressionType.Keyword), 4); 87 | Assert.AreEqual(CountExpressionsOfType(expF, ExpressionType.And), 3); 88 | Assert.AreEqual(CountExpressionsOfType(expF, ExpressionType.HasImages), 1); 89 | Assert.AreEqual(CountExpressionsOfType(expF, ExpressionType.IsRetweet), 1); 90 | } 91 | 92 | [TestMethod] 93 | public void FailParseExpression() 94 | { 95 | try 96 | { 97 | Expression.Parse("expression to fail: following_count:aaa..bbbb"); 98 | Assert.Fail("Test MUST fail! Not failed"); 99 | } 100 | catch (Exception e) 101 | { 102 | Assert.AreEqual(e.GetType(), typeof(ArgumentOutOfRangeException)); 103 | } 104 | } 105 | 106 | [TestMethod] 107 | public void ToExpression() 108 | { 109 | var rules = new ExpressionTest[] 110 | { 111 | new ("(\"Twitter API\" OR #v2) -\"recent search\""), // Group with different types 112 | new ("((\"Twitter API\" OR #v2) -\"recent search\") OR ((grumpy cat) OR (#meme has:images -is:retweet))"), // MultiLevelRule 113 | new ("apple OR iphone ipad OR iAd iCloud iCar", "apple OR (iphone ipad) OR (iAd iCloud iCar)"), // Mixed with no specific order 114 | 115 | // all examples on https://developer.twitter.com/en/docs/twitter-api/tweets/filtered-stream/integrate/build-a-rule 116 | new ("#twitterapiv2"), 117 | new ("\"twitter data\" has:mentions (has:media OR has:links)"), 118 | new ("snow day #NoSchool"), 119 | new ("grumpy OR cat OR #meme"), 120 | new ("cat #meme -grumpy"), 121 | new ("(grumpy cat) OR (#meme has:images)"), 122 | new ("skiing -(snow OR day OR noschool)"), // not good negation, but valid 123 | new ("skiing -snow -day -noschool"), // good negation 124 | new ("apple OR iphone ipad", "apple OR (iphone ipad)"), // Mixed with no specific order 125 | new ("apple OR (iphone ipad)"), // specific order 126 | new ("ipad iphone OR android", "(ipad iphone) OR android"), // Mixed with no specific order, but valid 127 | new ("(iphone ipad) OR android"), // specific order 128 | new ("diacrítica"), 129 | new ("#cumpleaños"), 130 | new ("happy"), 131 | new ("(happy OR happiness) place_country:GB -birthday -is:retweet"), 132 | new ("happy OR happiness"), 133 | new ("(happy OR happiness) lang:en"), 134 | new ("(happy OR happiness) lang:en -birthday -is:retweet"), 135 | new ("(happy OR happiness OR excited OR elated) lang:en -birthday -is:retweet"), 136 | new ("(happy OR happiness OR excited OR elated) lang:en -birthday -is:retweet -holidays"), 137 | new ("pepsi OR cola OR \"coca cola\""), 138 | new ("(\"Twitter API\" OR #v2) -\"filtered stream\""), 139 | new ("#thankunext #fanart OR @arianagrande", "(#thankunext #fanart) OR @arianagrande"), // Mixed with no specific order 140 | new ("(@twitterdev OR @twitterapi) -@twitter"), 141 | new ("$twtr OR @twitterdev -$fb", "$twtr OR (@twitterdev -$fb)"), // Mixed with no specific order, but valid 142 | new ("from:twitterdev OR from:twitterapi -from:twitter", "from:twitterdev OR (from:twitterapi -from:twitter)"), // Mixed with no specific order, but valid 143 | new ("to:twitterdev OR to:twitterapi -to:twitter", "to:twitterdev OR (to:twitterapi -to:twitter)"), // Mixed with no specific order, but valid 144 | new ("from:TwitterDev url:\"https://developer.twitter.com\""), 145 | new ("from:TwitterDev url:\"https://t.co\""), 146 | new ("url:\"https://developer.twitter.com\""), 147 | new ("retweets_of:twitterdev OR retweets_of:twitterapi"), 148 | new ("context:10.799022225751871488"), 149 | new ("context:47.*"), 150 | new ("context:*.799022225751871488"), 151 | new ("entity:\"string declaration of entity/place\""), 152 | new ("entity:\"Michael Jordan\" OR entity:\"Barcelona\""), // quote where no quotes needed 153 | new ("conversation_id:1334987486343299072 (from:twitterdev OR from:twitterapi)"), 154 | new ("bio:developer OR bio:\"data engineer\" OR bio:academic"), 155 | new ("bio_name:phd OR bio_name:md"), 156 | new ("bio_location:\"big apple\" OR bio_location:nyc OR bio_location:manhattan"), 157 | new ("place:\"new york city\" OR place:seattle OR place:fd70c22040963ac7"), 158 | new ("place_country:US OR place_country:MX OR place_country:CA"), 159 | new ("point_radius:[2.355128 48.861118 16km] OR point_radius:[-41.287336 174.761070 20mi]"), 160 | new ("bounding_box:[-105.301758 39.964069 -105.178505 40.09455]"), 161 | new ("data @twitterdev -is:retweet"), 162 | new ("from:twitterdev is:reply"), 163 | new ("\"sentiment analysis\" is:quote"), 164 | new ("#nowplaying is:verified"), 165 | new ("\"mobile games\" -is:nullcast"), 166 | new ("from:twitterdev -has:hashtags"), 167 | new ("#stonks has:cashtags"), 168 | new ("(kittens OR puppies) has:media"), 169 | new ("#meme has:images"), 170 | new ("#icebucketchallenge has:video_link", "#icebucketchallenge has:videos"), // Alias handling 171 | new ("recommend #paris has:geo -bakery"), 172 | new ("#nowplaying @spotify sample:15"), 173 | new ("recommend #paris lang:en"), 174 | new ("followers_count:1000..10000"), 175 | new ("tweets_count:1000..10000"), 176 | new ("following_count:500"), 177 | new ("following_count:1000..10000"), 178 | new ("listed_count:10"), 179 | new ("listed_count:10..100"), 180 | new ("url_title:snow"), 181 | new ("url_description:weather"), 182 | new ("url_contains:photos"), 183 | new ("source:\"Twitter for iPhone\""), 184 | new ("in_reply_to_tweet_id:1539382664746020864"), 185 | new ("retweets_of_tweet_id:1539382664746020864"), 186 | }; 187 | 188 | foreach (var rule in rules) 189 | { 190 | var expression = Expression.Parse(rule.ExpressionString); 191 | // strip extra brackets 192 | var expressionString = (expression.Type == ExpressionType.And || expression.Type == ExpressionType.Or) && !expression.IsNegate ? expression.ToString().Substring(1, expression.ToString().Length - 2) : expression.ToString(); 193 | Assert.AreEqual(rule.ExpectedString, expressionString); 194 | } 195 | } 196 | 197 | private class ExpressionTest 198 | { 199 | public string ExpressionString { get; set; } 200 | public string ExpectedString { get; set; } 201 | public ExpressionTest(string expressionString, string expectedString = "") 202 | { 203 | ExpressionString = expressionString; 204 | ExpectedString = expectedString == "" ? expressionString : expectedString; 205 | } 206 | } 207 | } 208 | } -------------------------------------------------------------------------------- /test/TestFollow.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using System; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using TwitterSharp.Client; 6 | using TwitterSharp.Request.Option; 7 | using TwitterSharp.Response.RUser; 8 | 9 | namespace TwitterSharp.UnitTests 10 | { 11 | [TestClass] 12 | public class TestFollow 13 | { 14 | private async Task ContainsFollowAsync(string username, RUsers rUsers) 15 | { 16 | if (rUsers.Users.Any(x => x.Username == username)) 17 | { 18 | return true; 19 | } 20 | if (rUsers.NextAsync == null) 21 | { 22 | return false; 23 | } 24 | return await ContainsFollowAsync(username, await rUsers.NextAsync()); 25 | } 26 | 27 | [TestMethod] 28 | public async Task GetUserFollowers() 29 | { 30 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 31 | var answer = await client.GetFollowersAsync("1022468464513089536", new UserSearchOptions 32 | { 33 | Limit = 1000 34 | }); 35 | Assert.IsTrue(await ContainsFollowAsync("CoreDesign_com", answer)); 36 | } 37 | 38 | [TestMethod] 39 | public async Task GetUserFollowing() 40 | { 41 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 42 | var answer = await client.GetFollowingAsync("1433657158067896325", new UserSearchOptions 43 | { 44 | Limit = 1000 45 | }); 46 | Assert.IsTrue(await ContainsFollowAsync("cover_corp", answer)); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/TestLike.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using System; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using TwitterSharp.Client; 6 | using TwitterSharp.Request.Option; 7 | using TwitterSharp.Response.RUser; 8 | 9 | namespace TwitterSharp.UnitTests 10 | { 11 | [TestClass] 12 | public class TestLike 13 | { 14 | private async Task ContainsLikeAsync(string username, RUsers rUsers) 15 | { 16 | if (rUsers.Users.Any(x => x.Username == username)) 17 | { 18 | return true; 19 | } 20 | if (rUsers.NextAsync == null) 21 | { 22 | return false; 23 | } 24 | return await ContainsLikeAsync(username, await rUsers.NextAsync()); 25 | } 26 | 27 | [TestMethod] 28 | public async Task GetTweetLikes() 29 | { 30 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 31 | var answer = await client.GetLikesAsync("1390699421726253063", new UserSearchOptions 32 | { 33 | Limit = 100 34 | }); 35 | Assert.IsTrue(await ContainsLikeAsync("daphne637", answer)); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/TestMedia.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using System; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using TwitterSharp.Client; 6 | using TwitterSharp.Request.AdvancedSearch; 7 | using TwitterSharp.Request.Option; 8 | using TwitterSharp.Response.RMedia; 9 | 10 | namespace TwitterSharp.UnitTests 11 | { 12 | [TestClass] 13 | public class TestMedia 14 | { 15 | [TestMethod] 16 | public async Task GetTweetWithoutMedia() 17 | { 18 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 19 | var answer = await client.GetTweetsAsync(new[] { "1237543996861251586" }); 20 | Assert.IsTrue(answer.Length == 1); 21 | var a = answer[0]; 22 | Assert.IsNull(a.Attachments); 23 | } 24 | 25 | [TestMethod] 26 | public async Task GetTweetWithMediaId() 27 | { 28 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 29 | var answer = await client.GetTweetsAsync(new[] { "1237543996861251586" }, new TweetSearchOptions 30 | { 31 | TweetOptions = new[] { TweetOption.Attachments_Ids } 32 | }); 33 | Assert.IsTrue(answer.Length == 1); 34 | var a = answer[0]; 35 | Assert.IsNotNull(a.Attachments); 36 | Assert.IsNotNull(a.Attachments.Media); 37 | Assert.AreEqual(1, a.Attachments.Media.Length); 38 | Assert.AreEqual("7_1237543944570847233", a.Attachments.Media[0].Key); 39 | Assert.IsNull(a.Attachments.Media[0].Type); 40 | Assert.IsNull(a.Attachments.Media[0].PreviewImageUrl); 41 | } 42 | 43 | [TestMethod] 44 | public async Task GetTweetWithMedia() 45 | { 46 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 47 | var answer = await client.GetTweetsAsync(new[] { "1237543996861251586" }, new TweetSearchOptions 48 | { 49 | TweetOptions = new[] { TweetOption.Attachments } 50 | }); 51 | Assert.IsTrue(answer.Length == 1); 52 | var a = answer[0]; 53 | Assert.IsNotNull(a.Attachments); 54 | Assert.IsNotNull(a.Attachments.Media); 55 | Assert.AreEqual(1, a.Attachments.Media.Length); 56 | Assert.AreEqual("7_1237543944570847233", a.Attachments.Media[0].Key); 57 | Assert.IsNotNull(a.Attachments.Media[0].Type); 58 | Assert.AreEqual(MediaType.Video, a.Attachments.Media[0].Type); 59 | Assert.IsNull(a.Attachments.Media[0].PreviewImageUrl); 60 | } 61 | 62 | [TestMethod] 63 | public async Task GetTweetWithMediaPreview() 64 | { 65 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 66 | var answer = await client.GetTweetsAsync(new[] { "1237543996861251586" }, new TweetSearchOptions 67 | { 68 | TweetOptions = new[] { TweetOption.Attachments }, 69 | MediaOptions = new[] { MediaOption.Preview_Image_Url } 70 | }); 71 | Assert.IsTrue(answer.Length == 1); 72 | var a = answer[0]; 73 | Assert.IsNotNull(a.Attachments); 74 | Assert.IsNotNull(a.Attachments.Media); 75 | Assert.AreEqual(1, a.Attachments.Media.Length); 76 | Assert.AreEqual("7_1237543944570847233", a.Attachments.Media[0].Key); 77 | Assert.IsNotNull(a.Attachments.Media[0].Type); 78 | Assert.AreEqual(MediaType.Video, a.Attachments.Media[0].Type); 79 | Assert.IsNotNull(a.Attachments.Media[0].PreviewImageUrl); 80 | Assert.AreEqual("https://pbs.twimg.com/ext_tw_video_thumb/1237543944570847233/pu/img/kRBUlSd7M7ju_QK1.jpg", a.Attachments.Media[0].PreviewImageUrl); 81 | } 82 | 83 | [TestMethod] 84 | public async Task GetTweetWithVideoMediaVariants() 85 | { 86 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 87 | var answer = await client.GetTweetsAsync(new[] { "1688848776612691968" }, new TweetSearchOptions 88 | { 89 | TweetOptions = new[] { TweetOption.Attachments }, 90 | MediaOptions = new [] { MediaOption.Variants } 91 | }); 92 | Assert.IsTrue(answer.Length == 1); 93 | var a = answer[0]; 94 | Assert.IsNotNull(a.Attachments); 95 | Assert.IsNotNull(a.Attachments.Media); 96 | Assert.AreEqual(1, a.Attachments.Media.Length); 97 | Assert.AreEqual("13_1688840150338428928", a.Attachments.Media[0].Key); 98 | Assert.IsNotNull(a.Attachments.Media[0].Type); 99 | Assert.AreEqual(MediaType.Video, a.Attachments.Media[0].Type); 100 | Assert.IsNull(a.Attachments.Media[0].PreviewImageUrl); 101 | 102 | Assert.IsNotNull(a.Attachments.Media[0].Variants); 103 | Assert.IsTrue(a.Attachments.Media[0].Variants.Length > 0); 104 | Assert.IsTrue(a.Attachments.Media[0].Variants.Any(v => v.ContentType == "video/mp4")); 105 | Assert.IsTrue(a.Attachments.Media[0].Variants.Any(v => v.BitRate.HasValue)); 106 | Assert.IsTrue(a.Attachments.Media[0].Variants.All(v => string.IsNullOrWhiteSpace(v.Url) == false)); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /test/TestRetweet.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using System; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using TwitterSharp.Client; 6 | using TwitterSharp.Request.Option; 7 | using TwitterSharp.Response.RUser; 8 | 9 | namespace TwitterSharp.UnitTests 10 | { 11 | [TestClass] 12 | public class TestRetweet 13 | { 14 | private async Task ContainsUserAsync(string username, RUsers rUsers) 15 | { 16 | if (rUsers.Users.Any(x => x.Username == username)) 17 | { 18 | return true; 19 | } 20 | if (rUsers.NextAsync == null) 21 | { 22 | return false; 23 | } 24 | return await ContainsUserAsync(username, await rUsers.NextAsync()); 25 | } 26 | 27 | [TestMethod] 28 | public async Task GetTweetRetweets() 29 | { 30 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 31 | var answer = await client.GetRetweetsAsync("1390699421726253063", new UserSearchOptions 32 | { 33 | Limit = 100 34 | }); 35 | Assert.IsTrue(await ContainsUserAsync("iamachibi", answer)); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/TestStream.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using TwitterSharp.ApiEndpoint; 7 | using TwitterSharp.Client; 8 | using TwitterSharp.Request; 9 | using TwitterSharp.Response; 10 | using TwitterSharp.Rule; 11 | 12 | namespace TwitterSharp.UnitTests 13 | { 14 | [TestClass] 15 | public class TestStream 16 | { 17 | [TestMethod] 18 | public async Task TestStreamProcess() 19 | { 20 | List rateLimitEvents = new(); 21 | 22 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 23 | 24 | client.RateLimitChanged += (_, rateLimit) => 25 | { 26 | rateLimitEvents.Add(rateLimit); 27 | }; 28 | 29 | var res = await client.GetInfoTweetStreamAsync(); 30 | var elem = res.FirstOrDefault(x => x.Tag == "TwitterSharp UnitTest"); 31 | 32 | var objectiveCount = res.Length + 1; 33 | 34 | if (elem != null) 35 | { 36 | await client.DeleteTweetStreamAsync(elem.Id); 37 | objectiveCount--; 38 | } 39 | 40 | var exp = Expression.Author("arurandeisu"); 41 | res = await client.AddTweetStreamAsync(new StreamRequest(exp, "TwitterSharp UnitTest")); 42 | 43 | Assert.IsTrue(res.Length == 1); 44 | Assert.IsTrue(res[0].Tag == "TwitterSharp UnitTest"); 45 | Assert.IsTrue(res[0].Value.ToString() == exp.ToString()); 46 | 47 | res = await client.GetInfoTweetStreamAsync(); 48 | 49 | Assert.IsTrue(CheckGetInfoTweetStreamAsyncRateLimit(rateLimitEvents)); 50 | 51 | elem = res.FirstOrDefault(x => x.Tag == "TwitterSharp UnitTest"); 52 | Assert.IsTrue(res.Length == objectiveCount); 53 | Assert.IsNotNull(elem.Id); 54 | Assert.IsTrue(elem.Tag == "TwitterSharp UnitTest"); 55 | Assert.IsTrue(elem.Value.ToString() == exp.ToString()); 56 | 57 | objectiveCount--; 58 | 59 | Assert.IsTrue(await client.DeleteTweetStreamAsync(elem.Id) == 1); 60 | 61 | res = await client.GetInfoTweetStreamAsync(); 62 | 63 | Assert.IsTrue(CheckGetInfoTweetStreamAsyncRateLimit(rateLimitEvents)); 64 | 65 | Assert.IsTrue(res.Length == objectiveCount); 66 | elem = res.FirstOrDefault(x => x.Tag == "TwitterSharp UnitTest"); 67 | Assert.IsNull(elem); 68 | } 69 | 70 | [TestMethod] 71 | public async Task TestStreamCancellation() 72 | { 73 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 74 | var requestSucceeded = false; 75 | var streamFinished = false; 76 | TaskStatus streamResult = TaskStatus.Created; 77 | 78 | client.RateLimitChanged += (_, _) => 79 | { 80 | requestSucceeded = true; 81 | }; 82 | 83 | _ = Task.Run(async () => 84 | { 85 | await client.NextTweetStreamAsync(_ => { }); 86 | }).ContinueWith(t => 87 | { 88 | streamResult = t.Status; 89 | streamFinished = true; 90 | }); 91 | 92 | // Test - IsStreaming 93 | while (!requestSucceeded) 94 | { 95 | await Task.Delay(25); 96 | } 97 | 98 | Assert.IsTrue(TwitterClient.IsTweetStreaming); 99 | 100 | // Test - two streams same client -> Exception 101 | try 102 | { 103 | await client.NextTweetStreamAsync(_ => { }); 104 | } 105 | catch (Exception e) 106 | { 107 | Assert.IsInstanceOfType(e, typeof(TwitterException)); 108 | } 109 | 110 | Assert.IsTrue(TwitterClient.IsTweetStreaming); 111 | 112 | // Test - two streams - different client -> Exception 113 | var client2 = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 114 | try 115 | { 116 | await client2.NextTweetStreamAsync(_ => { }); 117 | } 118 | catch (Exception e) 119 | { 120 | Assert.IsInstanceOfType(e, typeof(TwitterException)); 121 | } 122 | 123 | // Test - Cancel stream 124 | client.CancelTweetStream(); 125 | 126 | Assert.IsFalse(TwitterClient.IsTweetStreaming); 127 | 128 | while (!streamFinished) 129 | { 130 | await Task.Delay(25); 131 | } 132 | 133 | Assert.IsTrue(streamResult == TaskStatus.RanToCompletion); 134 | } 135 | 136 | [TestMethod] 137 | public async Task TestStreamErrorRule() 138 | { 139 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 140 | 141 | // Faulty expression 142 | var expression = Expression.Keyword("Faulty expression").And(Expression.PlaceCountry("xxxx")); 143 | 144 | try 145 | { 146 | await client.AddTweetStreamAsync(new StreamRequest(expression, "Test Error")); 147 | } 148 | catch (TwitterException e) 149 | { 150 | Assert.IsTrue(e.Errors != null); 151 | Assert.IsTrue(e.Errors.Length == 1); 152 | Assert.AreEqual("UnprocessableEntity", e.Errors.First().Title); 153 | Assert.IsTrue(e.Errors.First().Details.Length == 1); 154 | } 155 | 156 | // double faulty expression 157 | var expression2 = Expression.Keyword("double faulty expression").And(Expression.PlaceCountry("xxxx"), Expression.Sample(200)); 158 | 159 | try 160 | { 161 | await client.AddTweetStreamAsync(new StreamRequest(expression2, "Test Error 2")); 162 | } 163 | catch (TwitterException e) 164 | { 165 | Assert.IsTrue(e.Errors != null); 166 | Assert.IsTrue(e.Errors.Length == 1); 167 | Assert.AreEqual("UnprocessableEntity", e.Errors.First().Title); 168 | Assert.IsTrue(e.Errors.First().Details.Length == 2); 169 | } 170 | } 171 | 172 | private bool CheckGetInfoTweetStreamAsyncRateLimit(List rateLimitEvents) 173 | { 174 | var rateLimits = rateLimitEvents.Where(x => x.Endpoint == Endpoint.ListingFilters).ToList(); 175 | 176 | return rateLimits[^1].Remaining == rateLimits[^2].Remaining - 1; 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /test/TestTweet.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using System; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using TwitterSharp.Client; 6 | using TwitterSharp.Request.AdvancedSearch; 7 | using TwitterSharp.Request.Option; 8 | using TwitterSharp.Response.RMedia; 9 | using TwitterSharp.Response.RTweet; 10 | using TwitterSharp.Rule; 11 | 12 | namespace TwitterSharp.UnitTests 13 | { 14 | [TestClass] 15 | public class TestTweet 16 | { 17 | [TestMethod] 18 | public async Task GetTweetByIdsAsync() 19 | { 20 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 21 | var answer = await client.GetTweetsAsync(new[] { "1389189291582967809" }); 22 | Assert.IsTrue(answer.Length == 1); 23 | Assert.AreEqual("1389189291582967809", answer[0].Id); 24 | Assert.AreEqual("たのしみ!!\uD83D\uDC93 https://t.co/DgBYVYr9lN", answer[0].Text); 25 | Assert.IsNull(answer[0].Author); 26 | Assert.IsNull(answer[0].PossiblySensitive); 27 | } 28 | 29 | [TestMethod] 30 | public async Task GetTweetByIdAsync() 31 | { 32 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 33 | var answer = await client.GetTweetAsync("1389189291582967809"); 34 | Assert.AreEqual("1389189291582967809", answer.Id); 35 | Assert.AreEqual("たのしみ!!\uD83D\uDC93 https://t.co/DgBYVYr9lN", answer.Text); 36 | Assert.IsNull(answer.Author); 37 | Assert.IsNull(answer.PossiblySensitive); 38 | } 39 | 40 | [TestMethod] 41 | public async Task GetTweetByIdsWithAuthorAndSensitivityAsync() 42 | { 43 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 44 | var answer = await client.GetTweetsAsync(new[] { "1389189291582967809" }, new TweetSearchOptions 45 | { 46 | TweetOptions = new[] { TweetOption.Possibly_Sensitive }, 47 | UserOptions = Array.Empty() 48 | }); 49 | Assert.IsTrue(answer.Length == 1); 50 | Assert.AreEqual("1389189291582967809", answer[0].Id); 51 | Assert.AreEqual("たのしみ!!\uD83D\uDC93 https://t.co/DgBYVYr9lN", answer[0].Text); 52 | Assert.IsNotNull(answer[0].Author); 53 | Assert.IsNotNull(answer[0].PossiblySensitive); 54 | Assert.AreEqual("kiryucoco", answer[0].Author.Username); 55 | Assert.IsFalse(answer[0].PossiblySensitive.Value); 56 | } 57 | 58 | [TestMethod] 59 | public async Task GetTweetsAsync() 60 | { 61 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 62 | var answer = await client.GetTweetsAsync(new[] { "1389330151779930113", "1389331863102128130" }); 63 | Assert.IsTrue(answer.Length == 2); 64 | Assert.AreEqual("1389330151779930113", answer[0].Id); 65 | Assert.AreEqual("ねむくなーい!ねむくないねむくない!ドタドタドタドタ", answer[0].Text); 66 | Assert.IsNull(answer[0].Author); 67 | Assert.AreEqual("1389331863102128130", answer[1].Id); 68 | Assert.AreEqual("( - ω・ )", answer[1].Text); 69 | Assert.IsNull(answer[1].Author); 70 | } 71 | 72 | [TestMethod] 73 | public async Task GetTweetsByIdsWithAuthorAsync() 74 | { 75 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 76 | var answer = await client.GetTweetsAsync(new[] { "1389330151779930113", "1389331863102128130" }, new() 77 | { 78 | UserOptions = Array.Empty() 79 | }); 80 | Assert.IsTrue(answer.Length == 2); 81 | Assert.AreEqual("1389330151779930113", answer[0].Id); 82 | Assert.AreEqual("ねむくなーい!ねむくないねむくない!ドタドタドタドタ", answer[0].Text); 83 | Assert.IsNotNull(answer[0].Author); 84 | Assert.AreEqual("tsunomakiwatame", answer[0].Author.Username); 85 | Assert.AreEqual("1389331863102128130", answer[1].Id); 86 | Assert.AreEqual("( - ω・ )", answer[1].Text); 87 | Assert.IsNotNull(answer[1].Author); 88 | Assert.AreEqual("tsunomakiwatame", answer[1].Author.Username); 89 | } 90 | 91 | [TestMethod] 92 | public async Task GetTweetsFromUserIdAsync() 93 | { 94 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 95 | var answer = await client.GetTweetsFromUserIdAsync("1109748792721432577"); 96 | Assert.AreEqual(10, answer.Length); 97 | Assert.IsNull(answer[0].Author); 98 | } 99 | 100 | [TestMethod] 101 | public async Task GetTweetsFromUserIdWithSinceIdAsync() 102 | { 103 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 104 | var answer = await client.GetTweetsFromUserIdAsync("1200397238788247552", new TweetSearchOptions 105 | { 106 | SinceId = "1410551383795781634" 107 | }); 108 | Assert.AreEqual(2, answer.Length); 109 | } 110 | 111 | [TestMethod] 112 | public async Task GetTweetsFromUserIdWithStartTimeAsync() 113 | { 114 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 115 | var answer = await client.GetTweetsFromUserIdAsync("1200397238788247552", new TweetSearchOptions 116 | { 117 | StartTime = new DateTime(2021, 7, 1, 12, 50, 0) 118 | }); 119 | Assert.AreEqual(1, answer.Length); 120 | } 121 | 122 | [TestMethod] 123 | public async Task GetTweetsFromUserIdWithArgumentsAsync() // Issue #2 124 | { 125 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 126 | var answer = await client.GetTweetsFromUserIdAsync("1109748792721432577", new TweetSearchOptions 127 | { 128 | TweetOptions = new[] { TweetOption.Attachments }, 129 | UserOptions = Array.Empty() 130 | }); 131 | Assert.AreEqual(10, answer.Length); 132 | Assert.IsNotNull(answer[0].Author); 133 | } 134 | 135 | [TestMethod] 136 | public async Task GetTweetsFromUserIdWithAuthorAsync() 137 | { 138 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 139 | var answer = await client.GetTweetsFromUserIdAsync("1109748792721432577", new TweetSearchOptions 140 | { 141 | UserOptions = Array.Empty() 142 | }); 143 | Assert.IsTrue(answer.Length == 10); 144 | foreach (var t in answer) 145 | { 146 | Assert.IsNotNull(t.Author); 147 | Assert.AreEqual("inugamikorone", t.Author.Username); 148 | } 149 | } 150 | 151 | [TestMethod] 152 | public async Task GetTweetsFromUserIdWithModifiedLimitAsync() 153 | { 154 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 155 | var answer = await client.GetTweetsFromUserIdAsync("1109748792721432577", new() 156 | { 157 | Limit = 100 158 | }); 159 | Assert.IsTrue(answer.Length == 100); 160 | } 161 | 162 | [TestMethod] 163 | public async Task GetTweetWithNothing() 164 | { 165 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 166 | var a = await client.GetTweetAsync("1390702559086596101"); 167 | 168 | Assert.IsNull(a.Attachments); 169 | Assert.IsNull(a.ConversationId); 170 | Assert.IsNull(a.CreatedAt); 171 | Assert.IsNull(a.Entities); 172 | Assert.IsNull(a.InReplyToUserId); 173 | Assert.IsNull(a.Lang); 174 | Assert.IsNull(a.PossiblySensitive); 175 | Assert.IsNull(a.PublicMetrics); 176 | Assert.IsNull(a.ReferencedTweets); 177 | Assert.IsNull(a.ReplySettings); 178 | } 179 | 180 | [TestMethod] 181 | public async Task GetTweetWithAttachment() 182 | { 183 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 184 | var a = await client.GetTweetAsync("1481598340051914753", new TweetSearchOptions 185 | { 186 | TweetOptions = new[] { TweetOption.Attachments }, 187 | MediaOptions = new[] { MediaOption.Url } 188 | }); 189 | 190 | Assert.IsNotNull(a.Attachments); 191 | Assert.IsNotNull(a.Attachments.Media); 192 | Assert.IsTrue(a.Attachments.Media.Length == 1); 193 | Assert.IsTrue(a.Attachments.Media[0].Type == MediaType.Photo); 194 | Assert.IsTrue(a.Attachments.Media[0].Url == "https://pbs.twimg.com/media/FI-xd52aUAEHCJB.jpg"); 195 | Assert.IsNull(a.ConversationId); 196 | Assert.IsNull(a.CreatedAt); 197 | Assert.IsNull(a.Entities); 198 | Assert.IsNull(a.InReplyToUserId); 199 | Assert.IsNull(a.Lang); 200 | Assert.IsNull(a.PossiblySensitive); 201 | Assert.IsNull(a.PublicMetrics); 202 | Assert.IsNull(a.ReferencedTweets); 203 | Assert.IsNull(a.ReplySettings); 204 | } 205 | 206 | [TestMethod] 207 | public async Task GetTweetWithConversationId() 208 | { 209 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 210 | var a = await client.GetTweetAsync("1390738061797953536", new TweetSearchOptions 211 | { 212 | TweetOptions = new[] { TweetOption.Conversation_Id } 213 | }); 214 | 215 | Assert.IsNull(a.Attachments); 216 | Assert.IsNotNull(a.ConversationId); 217 | Assert.AreEqual("1390736182720430082", a.ConversationId); 218 | Assert.IsNull(a.CreatedAt); 219 | Assert.IsNull(a.Entities); 220 | Assert.IsNull(a.InReplyToUserId); 221 | Assert.IsNull(a.Lang); 222 | Assert.IsNull(a.PossiblySensitive); 223 | Assert.IsNull(a.PublicMetrics); 224 | Assert.IsNull(a.ReferencedTweets); 225 | Assert.IsNull(a.ReplySettings); 226 | } 227 | 228 | [TestMethod] 229 | public async Task GetTweetWithCreatedAt() 230 | { 231 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 232 | var a = await client.GetTweetAsync("1390699421726253063", new TweetSearchOptions 233 | { 234 | TweetOptions = new[] { TweetOption.Created_At } 235 | }); 236 | 237 | Assert.IsNull(a.Attachments); 238 | Assert.IsNull(a.ConversationId); 239 | Assert.IsNotNull(a.CreatedAt); 240 | Assert.AreEqual(new DateTime(2021, 5, 7, 16, 5, 54), a.CreatedAt); 241 | Assert.IsNull(a.Entities); 242 | Assert.IsNull(a.InReplyToUserId); 243 | Assert.IsNull(a.Lang); 244 | Assert.IsNull(a.PossiblySensitive); 245 | Assert.IsNull(a.PublicMetrics); 246 | Assert.IsNull(a.ReferencedTweets); 247 | Assert.IsNull(a.ReplySettings); 248 | } 249 | 250 | [TestMethod] 251 | public async Task GetTweetWithEntities() 252 | { 253 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 254 | var a = await client.GetTweetAsync("1390699421726253063", new TweetSearchOptions 255 | { 256 | TweetOptions = new[] { TweetOption.Entities } 257 | }); 258 | 259 | Assert.IsNull(a.Attachments); 260 | Assert.IsNull(a.ConversationId); 261 | Assert.IsNull(a.CreatedAt); 262 | Assert.IsNotNull(a.Entities); 263 | Assert.AreEqual(1, a.Entities.Hashtags.Length); 264 | Assert.AreEqual(0, a.Entities.Cashtags.Length); 265 | Assert.AreEqual(1, a.Entities.Urls.Length); 266 | Assert.AreEqual(0, a.Entities.Mentions.Length); 267 | Assert.IsNull(a.InReplyToUserId); 268 | Assert.IsNull(a.Lang); 269 | Assert.IsNull(a.PossiblySensitive); 270 | Assert.IsNull(a.PublicMetrics); 271 | Assert.IsNull(a.ReferencedTweets); 272 | Assert.IsNull(a.ReplySettings); 273 | } 274 | 275 | [TestMethod] 276 | public async Task GetTweetWithInReplyToUserId() 277 | { 278 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 279 | var a = await client.GetTweetAsync("1390700491294724099", new TweetSearchOptions 280 | { 281 | TweetOptions = new[] { TweetOption.In_Reply_To_User_Id } 282 | }); 283 | 284 | Assert.IsNull(a.Attachments); 285 | Assert.IsNull(a.ConversationId); 286 | Assert.IsNull(a.CreatedAt); 287 | Assert.IsNull(a.Entities); 288 | Assert.IsNotNull(a.InReplyToUserId); 289 | Assert.AreEqual("960340787782299648", a.InReplyToUserId); 290 | Assert.IsNull(a.Lang); 291 | Assert.IsNull(a.PossiblySensitive); 292 | Assert.IsNull(a.PublicMetrics); 293 | Assert.IsNull(a.ReferencedTweets); 294 | Assert.IsNull(a.ReplySettings); 295 | } 296 | 297 | [TestMethod] 298 | public async Task GetTweetWithLang() 299 | { 300 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 301 | var a = await client.GetTweetAsync("1390766509610340354", new TweetSearchOptions 302 | { 303 | TweetOptions = new[] { TweetOption.Lang } 304 | }); 305 | 306 | Assert.IsNull(a.Attachments); 307 | Assert.IsNull(a.ConversationId); 308 | Assert.IsNull(a.CreatedAt); 309 | Assert.IsNull(a.Entities); 310 | Assert.IsNull(a.InReplyToUserId); 311 | Assert.IsNotNull(a.Lang); 312 | Assert.AreEqual("ja", a.Lang); 313 | Assert.IsNull(a.PossiblySensitive); 314 | Assert.IsNull(a.PublicMetrics); 315 | Assert.IsNull(a.ReferencedTweets); 316 | Assert.IsNull(a.ReplySettings); 317 | } 318 | 319 | [TestMethod] 320 | public async Task GetTweetWithPossiblySensitive() 321 | { 322 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 323 | var a = await client.GetTweetAsync("1373191601154007040", new TweetSearchOptions 324 | { 325 | TweetOptions = new[] { TweetOption.Possibly_Sensitive } 326 | }); 327 | 328 | Assert.IsNull(a.Attachments); 329 | Assert.IsNull(a.ConversationId); 330 | Assert.IsNull(a.CreatedAt); 331 | Assert.IsNull(a.Entities); 332 | Assert.IsNull(a.InReplyToUserId); 333 | Assert.IsNull(a.Lang); 334 | Assert.IsNotNull(a.PossiblySensitive); 335 | Assert.IsTrue(a.PossiblySensitive.Value); 336 | Assert.IsNull(a.PublicMetrics); 337 | Assert.IsNull(a.ReferencedTweets); 338 | Assert.IsNull(a.ReplySettings); 339 | } 340 | 341 | [TestMethod] 342 | public async Task GetTweetWithPublicMetrics() 343 | { 344 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 345 | var a = await client.GetTweetAsync("1603823063690199040", new TweetSearchOptions 346 | { 347 | TweetOptions = new[] { TweetOption.Public_Metrics } 348 | }); 349 | 350 | Assert.IsNull(a.Attachments); 351 | Assert.IsNull(a.ConversationId); 352 | Assert.IsNull(a.CreatedAt); 353 | Assert.IsNull(a.Entities); 354 | Assert.IsNull(a.InReplyToUserId); 355 | Assert.IsNull(a.Lang); 356 | Assert.IsNull(a.PossiblySensitive); 357 | Assert.IsNotNull(a.PublicMetrics); 358 | Assert.IsTrue(a.PublicMetrics.LikeCount > 0); 359 | Assert.IsTrue(a.PublicMetrics.ReplyCount > 0); 360 | Assert.IsTrue(a.PublicMetrics.RetweetCount > 0); 361 | Assert.IsTrue(a.PublicMetrics.ImpressionCount > 0); 362 | Assert.IsNull(a.ReferencedTweets); 363 | Assert.IsNull(a.ReplySettings); 364 | } 365 | 366 | [TestMethod] 367 | public async Task GetTweetWithReferencedTweets() 368 | { 369 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 370 | var a = await client.GetTweetAsync("1387457640414859267", new TweetSearchOptions 371 | { 372 | TweetOptions = new[] { TweetOption.Referenced_Tweets } 373 | }); 374 | 375 | Assert.IsNull(a.Attachments); 376 | Assert.IsNull(a.ConversationId); 377 | Assert.IsNull(a.CreatedAt); 378 | Assert.IsNull(a.Entities); 379 | Assert.IsNull(a.InReplyToUserId); 380 | Assert.IsNull(a.Lang); 381 | Assert.IsNull(a.PossiblySensitive); 382 | Assert.IsNull(a.PublicMetrics); 383 | Assert.IsNotNull(a.ReferencedTweets); 384 | Assert.AreEqual("1387454731212103680", a.ReferencedTweets[0].Id); 385 | Assert.IsNull(a.ReplySettings); 386 | } 387 | 388 | [TestMethod] 389 | public async Task GetTweetWithReplySettings() 390 | { 391 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 392 | var a = await client.GetTweetAsync("1390650205440147457", new TweetSearchOptions 393 | { 394 | TweetOptions = new[] { TweetOption.Reply_Settings } 395 | }); 396 | 397 | Assert.IsNull(a.Attachments); 398 | Assert.IsNull(a.ConversationId); 399 | Assert.IsNull(a.CreatedAt); 400 | Assert.IsNull(a.Entities); 401 | Assert.IsNull(a.InReplyToUserId); 402 | Assert.IsNull(a.Lang); 403 | Assert.IsNull(a.PossiblySensitive); 404 | Assert.IsNull(a.PublicMetrics); 405 | Assert.IsNull(a.ReferencedTweets); 406 | Assert.IsNotNull(a.ReplySettings); 407 | Assert.AreEqual(ReplySettings.Everyone, a.ReplySettings); 408 | } 409 | 410 | [TestMethod] 411 | public async Task GetRecentTweets() 412 | { 413 | var hashtag = "Test"; 414 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 415 | 416 | // note: retweets are truncated at 140 characters, so i had to exclude them for making the check reliable 417 | var a = await client.GetRecentTweets(Expression.Hashtag(hashtag).And(Expression.IsRetweet().Negate())); 418 | 419 | Assert.IsTrue(a.All(x => x.Text.Contains("#"+hashtag, StringComparison.InvariantCultureIgnoreCase))); 420 | } 421 | 422 | [TestMethod] 423 | public async Task GetTweetByIdErrorAsync() 424 | { 425 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 426 | try 427 | { 428 | await client.GetTweetAsync("FALSE_TWEET_ID"); 429 | } 430 | catch (TwitterException e) 431 | { 432 | Assert.IsTrue(e.Errors != null); 433 | Assert.IsTrue(e.Errors.Length == 1); 434 | Assert.AreEqual("Invalid Request", e.Title); 435 | } 436 | } 437 | 438 | [TestMethod] 439 | public async Task GetTweetsByIdErrorAsync() 440 | { 441 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 442 | try 443 | { 444 | await client.GetTweetsAsync(new [] {"FALSE_TWEET_ID", "FALSE_TWEET_ID2"}); 445 | } 446 | catch (TwitterException e) 447 | { 448 | Assert.IsTrue(e.Errors != null); 449 | Assert.IsTrue(e.Errors.Length == 2); 450 | Assert.AreEqual("Invalid Request", e.Title); 451 | } 452 | } 453 | 454 | [TestMethod] 455 | public async Task GetTweetsFromUserIdErrorAsync() 456 | { 457 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 458 | try 459 | { 460 | await client.GetTweetsFromUserIdAsync("FALSE_USER_ID"); 461 | } 462 | catch (TwitterException e) 463 | { 464 | Assert.IsTrue(e.Errors != null); 465 | Assert.IsTrue(e.Errors.Length == 1); 466 | Assert.AreEqual("Invalid Request", e.Title); 467 | } 468 | } 469 | 470 | [TestMethod] 471 | public async Task GetTweetsFromNotFoundUserIdErrorAsync() 472 | { 473 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 474 | try 475 | { 476 | await client.GetTweetsFromUserIdAsync("1474083406862782466"); // Not found 477 | } 478 | catch (TwitterException e) 479 | { 480 | Assert.IsTrue(e.Errors != null); 481 | Assert.IsTrue(e.Errors.Length == 1); 482 | Assert.AreEqual("id", e.Errors.First().Parameter); 483 | Assert.AreEqual("Not Found Error", e.Errors.First().Title); 484 | } 485 | } 486 | 487 | [TestMethod] 488 | public async Task GetTweetsEditFromTweet() 489 | { 490 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 491 | var a = await client.GetTweetAsync("1575590534529556480", new TweetSearchOptions 492 | { 493 | TweetOptions = new[] { TweetOption.Edit_Controls } 494 | }); 495 | 496 | Assert.IsNull(a.Attachments); 497 | Assert.IsNull(a.ConversationId); 498 | Assert.IsNull(a.CreatedAt); 499 | Assert.IsNull(a.Entities); 500 | Assert.IsNull(a.InReplyToUserId); 501 | Assert.IsNull(a.Lang); 502 | Assert.IsNull(a.PossiblySensitive); 503 | Assert.IsNull(a.PublicMetrics); 504 | Assert.IsNull(a.ReferencedTweets); 505 | Assert.IsTrue(a.EditHistoryTweetIds.Length > 1); 506 | Assert.IsNotNull(a.EditControls); 507 | Assert.IsTrue(a.EditControls.IsEditEligible); 508 | Assert.IsTrue(a.EditControls.EditsRemaining > 0); 509 | Assert.IsTrue(a.EditControls.EditableUntil.Ticks == 638000835290000000); 510 | } 511 | } 512 | } 513 | -------------------------------------------------------------------------------- /test/TestUser.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using System; 3 | using System.Threading.Tasks; 4 | using TwitterSharp.Client; 5 | using TwitterSharp.Request.AdvancedSearch; 6 | using TwitterSharp.Request.Option; 7 | 8 | namespace TwitterSharp.UnitTests 9 | { 10 | [TestClass] 11 | public class TestUser 12 | { 13 | [TestMethod] 14 | public async Task GetUserAsync() 15 | { 16 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 17 | var answer = await client.GetUserAsync("theindra5"); 18 | Assert.AreEqual("1022468464513089536", answer.Id); 19 | Assert.AreEqual("TheIndra5", answer.Username); 20 | Assert.AreEqual("Indra", answer.Name); 21 | } 22 | 23 | [TestMethod] 24 | public async Task GetUserById() 25 | { 26 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 27 | var answer = await client.GetUserByIdAsync("1022468464513089536"); 28 | Assert.AreEqual("1022468464513089536", answer.Id); 29 | Assert.AreEqual("TheIndra5", answer.Username); 30 | Assert.AreEqual("Indra", answer.Name); 31 | } 32 | 33 | [TestMethod] 34 | public async Task GetUsersAsync() 35 | { 36 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 37 | var answer = await client.GetUsersAsync(new[] { "theindra5" }); 38 | Assert.IsTrue(answer.Length == 1); 39 | Assert.AreEqual("1022468464513089536", answer[0].Id); 40 | Assert.AreEqual("TheIndra5", answer[0].Username); 41 | Assert.AreEqual("Indra", answer[0].Name); 42 | } 43 | 44 | [TestMethod] 45 | public async Task GetUsersByIdsAsync() 46 | { 47 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 48 | var answer = await client.GetUsersByIdsAsync(new[] { "1022468464513089536" }); 49 | Assert.IsTrue(answer.Length == 1); 50 | Assert.AreEqual("1022468464513089536", answer[0].Id); 51 | Assert.AreEqual("TheIndra5", answer[0].Username); 52 | Assert.AreEqual("Indra", answer[0].Name); 53 | } 54 | 55 | [TestMethod] 56 | public async Task GetUserWithOptionsAsync() 57 | { 58 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 59 | var answer = await client.GetUsersAsync(new[] { "theindra5" }, new UserSearchOptions 60 | { 61 | UserOptions = new[] { UserOption.Description, UserOption.Public_Metrics, UserOption.Verified, UserOption.Protected } 62 | }); 63 | Assert.IsTrue(answer.Length == 1); 64 | Assert.AreEqual("1022468464513089536", answer[0].Id); 65 | Assert.AreEqual("TheIndra5", answer[0].Username); 66 | Assert.AreEqual("Indra", answer[0].Name); 67 | Assert.IsNotNull(answer[0].Description); 68 | Assert.IsNotNull(answer[0].PublicMetrics); 69 | Assert.IsFalse(answer[0].Verified != null && answer[0].Verified.Value); 70 | Assert.IsFalse(answer[0].Protected != null && answer[0].Protected.Value); 71 | Assert.IsNull(answer[0].VerifiedType); 72 | } 73 | 74 | [TestMethod] 75 | public async Task GetUserWithVerifiedTypeAsync() 76 | { 77 | var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); 78 | var answer = await client.GetUsersAsync(new[] { "theindra5", "TwitterDev", "NorwayMFA", "elonmusk" }, new UserSearchOptions 79 | { 80 | UserOptions = new[] { UserOption.Verified_Type } 81 | }); 82 | Assert.IsTrue(answer.Length == 4); 83 | Assert.IsTrue(answer[0].VerifiedType != null && answer[0].VerifiedType == "none"); 84 | Assert.IsTrue(answer[1].VerifiedType != null && answer[1].VerifiedType == "business"); 85 | Assert.IsTrue(answer[2].VerifiedType != null && answer[2].VerifiedType == "government"); 86 | Assert.IsTrue(answer[3].VerifiedType != null && answer[3].VerifiedType == "blue"); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /test/TwitterSharp.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0;net6.0;net7.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | --------------------------------------------------------------------------------