Compare commits

...
This repository has been archived on 2023-05-27. You can view files and clone it, but cannot push or open issues or pull requests.

120 Commits

Author SHA1 Message Date
Renovate Bot 41194d2bff chore(deps): update mstest monorepo to v3.0.3
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-05-25 09:00:52 +00:00
Renovate Bot d45df52e12 chore(deps): update dependency terminal.gui to v1.12.1
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-05-24 14:00:54 +00:00
Renovate Bot eb44e55cdc chore(deps): update dependency coverlet.collector to v6
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-05-21 14:01:02 +00:00
Renovate Bot f4d09d2307 chore(deps): update dependency terminal.gui to v1.11.2
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-05-19 16:00:46 +00:00
Renovate Bot 60d6f8cc85 chore(deps): update dependency terminal.gui to v1.11.1
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-05-19 09:00:53 +00:00
Renovate Bot 1474b9c760 chore(deps): update dependency microsoft.net.test.sdk to v17.6.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-05-16 14:00:47 +00:00
Renovate Bot d694320fd6 chore(deps): update dependency lamar.microsoft.dependencyinjection to v12
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-05-01 16:01:00 +00:00
Renovate Bot a1d8281fa3 chore(deps): update dependency lamar.microsoft.dependencyinjection to v11.1.4
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-23 16:00:54 +00:00
Renovate Bot a8f331c74b chore(deps): update dependency terminal.gui to v1.10.1
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-06 16:00:53 +00:00
Renovate Bot b0c520cc42 chore(deps): update dependency lamar.microsoft.dependencyinjection to v11.1.2
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-03-22 17:00:35 +00:00
Renovate Bot 5266fa62da chore(deps): update dependency microsoft.visualstudio.azure.containers.tools.targets to v1.18.1
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is failing Details
2023-03-20 23:00:38 +00:00
Renovate Bot 90f5e8db0e chore(deps): update dependency terminal.gui to v1.10.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-03-17 16:00:42 +00:00
Renovate Bot 5420593ca7 chore(deps): update dependency newtonsoft.json to v13.0.3
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-03-08 08:00:32 +00:00
Renovate Bot dcddf6841c chore(deps): update dependency lamar.microsoft.dependencyinjection to v11
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-03-05 20:00:34 +00:00
Renovate Bot a80ea0e97a chore(deps): update dependency microsoft.net.test.sdk to v17.5.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-02-21 11:00:36 +00:00
Renovate Bot ac4a6f5e87 chore(deps): update dependency lamar.microsoft.dependencyinjection to v10.0.2
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-02-16 16:00:34 +00:00
Sam Therapy 1d516010c2
AAAAAAAAAAAAAAAA
continuous-integration/drone/push Build is passing Details
Signed-off-by: Sam Therapy <sam@samtherapy.net>
2023-02-02 21:54:17 +01:00
Sam Therapy ff7ebfc7fb
I HATE MY LIFE
continuous-integration/drone/push Build was killed Details
Signed-off-by: Sam Therapy <sam@samtherapy.net>
2023-02-02 21:46:13 +01:00
Sam Therapy 7e0207c248
I hate my life
continuous-integration/drone/push Build is passing Details
Signed-off-by: Sam Therapy <sam@samtherapy.net>
2023-02-02 21:36:31 +01:00
Sam Therapy 99bf1e1077
wait this isn't JavaScript
continuous-integration/drone/push Build is passing Details
Signed-off-by: Sam Therapy <sam@samtherapy.net>
2023-02-02 21:20:36 +01:00
Sam Therapy d22af149b5
fix(AP): add request header
continuous-integration/drone/push Build is passing Details
Signed-off-by: Sam Therapy <sam@samtherapy.net>
2023-02-02 21:07:10 +01:00
Sam Therapy a29a42c986
fix(BSLManager): use system console instead
continuous-integration/drone/push Build is passing Details
https://github.com/gui-cs/Terminal.Gui/issues/2085#issuecomment-1279695885
Signed-off-by: Sam Therapy <sam@samtherapy.net>
2023-01-25 15:36:41 +01:00
Renovate Bot f9efdbbac9 chore(deps): update dependency lamar.microsoft.dependencyinjection to v10
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-01-11 22:00:34 +00:00
Renovate Bot 3469b82df7 chore(deps): update dependency portable.bouncycastle to v1.9.0
continuous-integration/drone/push Build is passing Details
2023-01-11 21:27:04 +00:00
Renovate Bot f09d138f43 chore(deps): update dependency microsoft.applicationinsights.aspnetcore to v2.21.0
continuous-integration/drone/push Build is running Details
2023-01-11 21:26:56 +00:00
Renovate Bot 36fcde1a96 chore(deps): update dependency npgsql to v4.1.12
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is running Details
2023-01-11 21:01:07 +00:00
Renovate Bot 45d1b24e07 chore(deps): update dependency microsoft.visualstudio.web.codegeneration.design to v3.1.5
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-01-11 20:00:36 +00:00
Renovate Bot 12acc3364d chore(deps): update actions/setup-dotnet action to v3
continuous-integration/drone/push Build is passing Details
2023-01-11 19:33:05 +00:00
Renovate Bot 30b6db3678 chore(deps): update actions/checkout action to v3
continuous-integration/drone/push Build is running Details
2023-01-11 19:32:34 +00:00
Renovate Bot e8f0dc2963 chore(deps): update dependency microsoft.visualstudio.azure.containers.tools.targets to v1.17.0
continuous-integration/drone/push Build is running Details
2023-01-11 19:31:51 +00:00
Renovate Bot f605cd510b chore(deps): update dependency terminal.gui to v1.9.0
continuous-integration/drone/push Build is running Details
2023-01-11 19:31:31 +00:00
Renovate Bot df8a26e34a chore(deps): update dependency newtonsoft.json to v13
continuous-integration/drone/push Build is running Details
2023-01-11 19:31:10 +00:00
Renovate Bot 1957c48f06 chore(deps): update dependency lamar.microsoft.dependencyinjection to v5.0.4
continuous-integration/drone/push Build is running Details
2023-01-11 19:30:51 +00:00
Renovate Bot b63aace525 chore(deps): update dependency lamar to v5.0.4
continuous-integration/drone/push Build is running Details
2023-01-11 19:30:36 +00:00
Renovate Bot 0714b89599 chore(deps): update dependency dapper to v2.0.123
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is running Details
2023-01-11 19:00:34 +00:00
Sam Therapy 0b3c2c9f84
[SKIP CI] renovate :)
Signed-off-by: Sam Therapy <sam@samtherapy.net>
2023-01-11 19:04:50 +01:00
Sam Therapy debd7328c8
forgor the fork info
continuous-integration/drone/push Build is passing Details
Signed-off-by: Sam Therapy <sam@samtherapy.net>
2023-01-11 16:18:09 +01:00
Sam Therapy 1037449e18
Merge branch 'master' of https://github.com/NicolasConstant/BirdsiteLive
continuous-integration/drone/push Build is passing Details
2023-01-11 15:54:29 +01:00
Nicolas Constant 8b1a61c197
Merge pull request #179 from NicolasConstant/develop
0.22.0 PR
2022-12-30 16:03:24 -05:00
Nicolas Constant 8a3ca81731
road to 0.22.0 2022-12-30 15:54:06 -05:00
Nicolas Constant f489e03a2b
changed display order of migration info 2022-12-29 16:54:54 -05:00
Nicolas Constant cc37ed32c2
Merge pull request #177 from NicolasConstant/topic_federate-migration
Topic federate migration
2022-12-29 16:43:51 -05:00
Nicolas Constant 4461884975
added input checks 2022-12-28 18:29:57 -05:00
Nicolas Constant 21ff67e3a8
added checks 2022-12-28 18:21:10 -05:00
Nicolas Constant e950801f56
better loggin 2022-12-28 18:00:24 -05:00
Nicolas Constant eccd9bdd28
don't call itself 2022-12-28 17:56:25 -05:00
Nicolas Constant f45e9ed9f7
added migration calls 2022-12-28 17:52:05 -05:00
Nicolas Constant f7ca9fd86d
added robustness 2022-12-28 16:16:10 -05:00
Nicolas Constant d64063b273
added federationinfo service 2022-12-28 16:13:34 -05:00
Nicolas Constant 93110d9972
refactoring 2022-12-28 15:00:14 -05:00
Nicolas Constant 8897b8838d
Merge pull request #176 from NicolasConstant/develop
0.21.0 PR
2022-12-27 23:34:04 -05:00
Nicolas Constant 36d80be7cf
road to 0.21.0 2022-12-27 23:29:24 -05:00
Nicolas Constant 05cbddbf26
Merge pull request #175 from NicolasConstant/topic_user-migration
Topic user migration
2022-12-27 23:16:41 -05:00
Nicolas Constant f8a354d90b
clean up 2022-12-27 23:09:25 -05:00
Nicolas Constant 89289f2c3a
wording 2022-12-27 22:39:11 -05:00
Nicolas Constant e804b1929c
always show link 2022-12-27 22:35:58 -05:00
Nicolas Constant 0dfe1f4f9f
fixed moved to 2022-12-27 22:35:11 -05:00
Nicolas Constant 52da17393b
debug info 2022-12-27 22:34:55 -05:00
Nicolas Constant 7c267063f9
fix DM notification 2022-12-27 22:13:08 -05:00
Nicolas Constant ae42b109e9
fix profile 2022-12-25 19:20:07 -05:00
Nicolas Constant 27e735ca4d
better redirection 2022-12-25 18:57:31 -05:00
Nicolas Constant 0eb0aa3c5d
added migration message 2022-12-25 18:46:56 -05:00
Nicolas Constant 2dd1cc7381
wording 2022-12-25 18:37:05 -05:00
Nicolas Constant c910edc6b3
various logic fixes 2022-12-25 18:15:54 -05:00
Nicolas Constant d5a71bbaa6
fix route 2022-12-25 00:10:37 -05:00
Nicolas Constant ac297b815a
added account status check before migration/deletion 2022-12-25 00:09:38 -05:00
Nicolas Constant 1c3da007fd
don't retrieve deleted users 2022-12-24 18:44:41 -05:00
Nicolas Constant d219c59cfe
added delete event when deleting user 2022-12-21 01:06:45 -05:00
Nicolas Constant d543a1d4f9
added delete action 2022-12-21 00:25:43 -05:00
Nicolas Constant 7658438741
always notify 2022-12-20 18:47:21 -05:00
Nicolas Constant 1a939b6147
return gone on deleted state 2022-12-14 00:35:25 -05:00
Nicolas Constant 8840d1007c
fix notification 2022-12-14 00:17:00 -05:00
Nicolas Constant 9a6971c6bc
typo 2022-12-13 23:48:14 -05:00
Nicolas Constant 2e5bb28ff8
updated mirror account data 2022-12-13 23:42:22 -05:00
Nicolas Constant 4157f613ea
added db migration + test 2022-12-13 23:02:28 -05:00
Nicolas Constant 6f8a2c0373
added deletion workflow 2022-12-13 22:55:22 -05:00
Nicolas Constant 4d365e2043
Merge pull request #174 from ElanHasson/patch-1
Fix typo
2022-12-12 22:15:05 -05:00
Elan Hasson d08caf3684
Fix typo 2022-12-12 19:57:29 -05:00
Nicolas Constant 86c852b8a8
added linux/amd64 reference 2022-11-13 23:01:51 +01:00
Nicolas Constant 1922b7dfc8
updating instance information 2022-11-09 20:13:22 -05:00
Nicolas Constant 4fb04c16b8
added better blacklisting handling 2022-11-04 03:14:00 -04:00
Nicolas Constant df68b9c370
added migration logic 2022-11-04 02:07:50 -04:00
Nicolas Constant 498134f215
added migrated tests 2022-11-03 20:18:45 -04:00
Nicolas Constant 76b2e659ab
added movedTo support in db 2022-11-03 20:02:37 -04:00
Nicolas Constant 15f0ad55ae
validation of the fediverse user 2022-11-02 01:15:05 -04:00
Nicolas Constant ec3234324c
validating tweet 2022-11-02 00:10:46 -04:00
Nicolas Constant cc9985eb1d
creating migration pages 2022-11-01 23:38:54 -04:00
Nicolas Constant 2880de9dda
Merge pull request #145 from nemobis/patch-1
Fix typo in README
2022-05-05 01:49:22 -04:00
nemobis 6df0529d0b
Fix typo in README 2022-04-10 19:27:17 +03:00
Nicolas Constant 4c4fc95da3
Merge pull request #139 from NicolasConstant/topic_better-progression-handling
remove users if not followed
2022-02-10 00:39:35 -05:00
Nicolas Constant 9415eb2e0c
remove users if not followed 2022-02-10 00:34:51 -05:00
Nicolas Constant ed3faab924
Merge pull request #138 from NicolasConstant/develop
0.20.0 PR
2022-02-09 18:43:51 -05:00
Nicolas Constant 7007b6309a
handle exception in sharedInbox 2022-02-09 01:54:35 -05:00
Nicolas Constant d0dd317723
Merge pull request #137 from NicolasConstant/topic_handle-gone-followers
added user is gone exception
2022-02-09 01:27:59 -05:00
Nicolas Constant 0e9938b712
added user is gone exception 2022-02-09 01:15:48 -05:00
Nicolas Constant e78bc262ed
added url support in webfinger 2022-02-08 23:40:02 -05:00
Nicolas Constant a7b4a4978a
throw exception instead of returning null 2022-02-08 20:32:48 -05:00
Nicolas Constant 4e9fec1a46
Merge pull request #135 from NicolasConstant/docs
added details on standalone app API key
2022-02-07 20:58:45 -05:00
Nicolas Constant d59e89a901
added details on standalone app API key 2022-02-07 20:42:14 -05:00
Nicolas Constant b0e7601333
Merge pull request #134 from NicolasConstant/topic_add-proper-exceptions
Topic add proper exceptions handling
2022-02-07 19:45:49 -05:00
Nicolas Constant 9f9f88aab7
clean up 2022-02-07 19:37:28 -05:00
Nicolas Constant 420d8867e7
added test for new behavior 2022-02-07 19:36:19 -05:00
Nicolas Constant d1c5a59247
fix tests 2022-02-07 19:33:08 -05:00
Nicolas Constant 662f97e53c
added proper exception in user retrieval 2022-02-07 18:48:10 -05:00
Nicolas Constant 446b222881
Merge pull request #133 from NicolasConstant/topic_detect-saturation-from-api-itself
get rate limit from API
2022-02-03 19:48:52 -05:00
Nicolas Constant b116f6a3ce
Merge pull request #132 from NicolasConstant/topic_set-cache-in-settings
set the cache limits from settings
2022-02-03 19:48:31 -05:00
Nicolas Constant c043e0b6a0
get rate limit from API 2022-02-03 19:45:25 -05:00
Nicolas Constant 1536880c73
set the cache limits from settings 2022-02-03 19:01:21 -05:00
Nicolas Constant 25ba19bc4f
Merge pull request #131 from NicolasConstant/topic_prevent-twitter-api-spam
Topic prevent twitter api spam
2022-02-03 01:09:11 -05:00
Nicolas Constant bf7baba789
added proper return on TooManyRequest case 2022-02-03 01:06:32 -05:00
Nicolas Constant c371218672
prevent saturation of the user retrieval API 2022-02-02 23:25:03 -05:00
Nicolas Constant 18e0397efe
Merge pull request #130 from NicolasConstant/topic_purge-failing-users
Topic purge failing users
2022-02-02 22:09:19 -05:00
Nicolas Constant 15d7e87466
added FailingFollowerCleanUpThreshold variable 2022-02-02 21:47:02 -05:00
Nicolas Constant 3a998b60ac
added auto clean-up on failing follower 2022-02-02 21:33:45 -05:00
Nicolas Constant 26cca6a306
upgrade failing counter to integer 2022-01-27 22:52:59 -05:00
Nicolas Constant 5c4641c6ae
disable debuging features on release 2022-01-27 19:58:35 -05:00
Nicolas Constant 04b8cfa0e4
Merge pull request #127 from NicolasConstant/topic_support-delete
Topic support delete
2021-12-13 23:35:27 -05:00
Nicolas Constant a36171c163
road to 0.20.0 2021-12-13 23:29:40 -05:00
Nicolas Constant 7205a09eaa
added Delete logic 2021-12-13 20:43:57 -05:00
Nicolas Constant 93b43ee4a0
added achitecture to handle Delete action 2021-12-09 02:02:30 -05:00
96 changed files with 3206 additions and 426 deletions

59
.drone.yml Normal file
View File

@ -0,0 +1,59 @@
kind: pipeline
name: testing
type: docker
steps:
- name: Install Dependencies
image: mcr.microsoft.com/dotnet/sdk:6.0
commands:
- dotnet restore ./src
- name: Build
image: mcr.microsoft.com/dotnet/sdk:6.0
commands:
- dotnet build --configuration Release ./src
- name: Test
image: mcr.microsoft.com/dotnet/sdk:6.0
commands:
- sed -i "s/127\.0\.0\.1/database/g" ./src/Tests/BirdsiteLive.DAL.Postgres.Tests/DataAccessLayers/Base/PostgresTestingBase.cs
- dotnet test --verbosity minimal ./src
services:
- name: database
image: postgres:15
environment:
POSTGRES_USER: birdtest
POSTGRES_PASSWORD: mysecretpassword
POSTGRES_DB: birdsitetest
---
kind: pipeline
name: docker-publish
type: docker
depends_on:
- testing
steps:
- name: Build & Publish
privileged: true
image: quay.io/thegeeklab/drone-docker-buildx
settings:
auto_tag: true
repo: git.froth.zone/sam/birdsitelive
registry: git.froth.zone
username: sam
password:
from_secret: password
platforms:
- linux/amd64
- linux/arm64
when:
branch:
- master
event:
- push
depends_on:
- "clone"

View File

@ -10,11 +10,11 @@ jobs:
working-directory: ./src
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Launch Db for testing
run: docker run --name postgres -e POSTGRES_DB=mytestdb -e POSTGRES_PASSWORD=mysecretpassword -d -p 5432:5432 postgres
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
uses: actions/setup-dotnet@v3
with:
dotnet-version: 3.1.101
- name: Install dependencies

2
.gitignore vendored
View File

@ -352,3 +352,5 @@ MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
/src/BSLManager/Properties/launchSettings.json
.dccache

View File

@ -1,14 +1,15 @@
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM mcr.microsoft.com/dotnet/aspnet:3.1-buster-slim AS base
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:3.1-buster AS publish
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS publish
WORKDIR /
COPY ./src/ ./src/
RUN dotnet publish "/src/BirdsiteLive/BirdsiteLive.csproj" -c Release -o /app/publish
RUN dotnet publish "/src/BSLManager/BSLManager.csproj" -r linux-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeAllContentForSelfExtract=true -c Release -o /app/publish
RUN dotnet publish "/src/BirdsiteLive/BirdsiteLive.csproj" -c Release -o /app/publish \
&& dotnet publish "/src/BSLManager/BSLManager.csproj" -r linux-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeAllContentForSelfExtract=true -c Release -o /app/publish
FROM base AS final
WORKDIR /app

View File

@ -10,10 +10,10 @@ Your instance will need [docker](https://docs.docker.com/engine/install/) and [d
## Setup
Download the [docker-compose file](https://git.gamers.exposed/pasture/BirdsiteLIVE/raw/branch/master/docker-compose.yml):
Download the [docker-compose file](https://git.froth.zone/sam/BirdsiteLIVE/raw/branch/master/docker-compose.yml):
```
sudo curl -L https://git.gamers.exposed/pasture/BirdsiteLIVE/raw/branch/master/docker-compose.yml -o docker-compose.yml
sudo curl -L https://git.froth.zone/sam/BirdsiteLIVE/raw/branch/master/docker-compose.yml -o docker-compose.yml
```
Then edit file:
@ -26,7 +26,7 @@ sudo nano docker-compose.yml
#### Personal info
* `Instance:Domain` the domain name you'll be using, for example use `birdsite.live` for the URL `https://birdsite.live`
* `Instance:Domain` the domain name you'll be using, for example use `birdsite.example.com` for the URL `https://birdsite.example.com`
* `Instance:AdminEmail` the admin's email, will be displayed in the instance /.well-known/nodeinfo endpoint
* `Twitter:ConsumerKey` the Twitter API key
* `Twitter:ConsumerSecret` the Twitter API secret key
@ -55,35 +55,14 @@ docker-compose up -d
By default the app will be available on the port 5000
## Nginx
## Nginx configuration
On a Debian based distrib:
```
sudo apt update
sudo apt install nginx
```
Check nginx status:
```
sudo systemctl status nginx
```
### Create nginx configuration
Create your nginx configuration
```
sudo nano /etc/nginx/sites-enabled/{your-domain-name.com}
```
And fill your service block as follow:
Fill your service block as follow:
```
server {
listen 80;
server_name {your-domain-name.com};
server_name birdsite.example.com;
location / {
proxy_pass http://localhost:5000;
proxy_http_version 1.1;
@ -111,16 +90,31 @@ After having a domain name pointing to your instance, install and setup certbot:
```
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d {your-domain-name.com}
sudo certbot --nginx -d birdsite.example.com
```
Make sure you're redirecting all traffic to https when asked.
Finally check that the auto-revewal will work as espected:
Finally check that the auto-renewal will work as expected:
```
sudo certbot renew --dry-run
```
## Caddy
Or, you can use [caddy](https://caddyserver.com)
```caddyfile
birdsite.example.com {
encode gzip
header ?Cache-Control "max-age=3600"
reverse_proxy http://localhost:5000 {
header_down -Server
}
}
```
Everything
### Set the firewall

View File

@ -1,22 +1,15 @@
This project is a *fork* of [the original BirdsiteLIVE from NicolasConstant](https://github.com/NicolasConstant/BirdsiteLive). This fork runs in production on [a large BirdsiteLIVE instance](https://twtr.plus). Changes made in this fork include:
# BirdsiteLIVE: Twitter -> ActivityPub
* Rework About page entirely - also disclose unlisted accounts and federation restrictions
* Cache Tweets so that, for example, Announces do not hit rate limits
* Allow replacing and redirecting to twitter.com in Tweets to other domains (i.e. Nitter instances)
* Verified checkmarks on [verified](https://twitter.com/verified) Twitter users
* Proper remote follow form on user pages
* Mark individual Tweets as potentially sensitive
* Define and enforce a maximum follow count limit
* Define and enforce a maximum Tweet fetch age using snowflakes
* (Optional) send quote-RTs as Soapbox-style quote posts
[![Build Status](https://ci.git.froth.zone/api/badges/sam/BirdsiteLIVE/status.svg)](https://ci.git.froth.zone/sam/BirdsiteLIVE)
This fork is also available as a Docker image as `pasture/birdsitelive`.
This project is a _fork_ of [Pasture's fork](https://git.gamers.exposed/pasture/BirdsiteLIVE) of [the original BirdsiteLIVE from NicolasConstant](https://github.com/NicolasConstant/BirdsiteLive). This fork runs in production on [bird.froth.zone](https://bird.froth.zone). Changes made in this fork include:
The project's original README is as follows:
- Rebasing the forks together.
- (this space intentionally left blank)
![Test](https://github.com/NicolasConstant/BirdsiteLive/workflows/.NET%20Core/badge.svg?branch=master&event=push)
This fork is also available as a Docker image as `git.froth.zone/sam/birdsitelive`.
# BirdsiteLIVE
The project's original README is below:
## About
@ -24,24 +17,22 @@ BirdsiteLIVE is an ActivityPub bridge from Twitter, it's mostly a pet project/pl
## State of development
The code is pretty messy and far from a good state, since it's a playground for me the aim was to understand some AP concepts, not provide a good state-of-the-art codebase. But I might refactor it to make it cleaner.
The code is pretty messy and far from a good state, since it's a playground for me the aim was to understand some AP concepts, not to provide a good state-of-the-art codebase. But I might refactor it to make it cleaner.
## Official instance
## Official instance
You can find an official (and temporary) instance here: [beta.birdsite.live](https://beta.birdsite.live). This instance can disapear at any time, if you want a long term instance you should install your own or use another one.
There's none! Please read [here why I've stopped it](https://write.as/nicolas-constant/closing-the-official-bsl-instance).
## Installation
I'm providing a [docker build](https://hub.docker.com/r/nicolasconstant/birdsitelive). To install it on your own server, please follow [those instructions](https://github.com/NicolasConstant/BirdsiteLive/blob/master/INSTALLATION.md). More [options](https://github.com/NicolasConstant/BirdsiteLive/blob/master/VARIABLES.md) are also available.
I'm providing a [docker build](https://git.froth.zone/sam/-/packages/container/birdsitelive/latest). To install it on your own server, please follow [those instructions](./INSTALLATION.md). More [options](./VARIABLES.md) are also available.
Also a [CLI](https://github.com/NicolasConstant/BirdsiteLive/blob/master/BSLManager.md) is available for adminitrative tasks.
Also a (likely broken) [CLI](./BSLManager.md) is available for administrative tasks.
## License
This project is licensed under the AGPLv3 License - see [LICENSE](https://github.com/NicolasConstant/BirdsiteLive/blob/master/LICENSE) for details.
This project is licensed under the AGPLv3 License - see [LICENSE](./LICENSE) for details.
## Contact
You can contact me via ActivityPub <a rel="me" href="https://fosstodon.org/@BirdsiteLIVE">here</a>.

View File

@ -58,6 +58,8 @@ If both whitelisting and blacklisting are set, only the whitelisting will be act
* `Instance:FailingTwitterUserCleanUpThreshold` (default: 700) set the max allowed errors (due to a banned/deleted/private account) from a Twitter Account retrieval before auto-removal. (by default an account is called every 15 mins)
* `Instance:MaxStatusFetchAge` (default: 0 - no limit) statuses with a Snowflake older than this age in days will not be fetched by the service and will instead return 410 Gone
* `Instance:EnableQuoteRT` (default: false) enable Soapbox-style quote-RTs
* `Instance:FailingFollowerCleanUpThreshold` (default: 30000) set the max allowed errors from a Follower (Fediverse) Account before auto-removal. (often due to account suppression, instance issues, etc)
* `Instance:UserCacheCapacity` (default: 10000) set the caching limit of the Twitter User retrieval. Must be higher than the number of synchronized accounts on the instance.
# Docker Compose full example

View File

@ -6,7 +6,7 @@ networks:
services:
server:
image: pasture/birdsitelive:latest
image: git.froth.zone/birdsitelive:latest
restart: always
container_name: birdsitelive
environment:
@ -27,7 +27,7 @@ services:
- db
db:
image: postgres:13
image: postgres:15
restart: always
environment:
- POSTGRES_USER=birdsitelive

12
renovate.json Normal file
View File

@ -0,0 +1,12 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base",
":npm",
":gomod",
":pinSkipCi",
":docker",
":enableVulnerabilityAlerts",
":semanticCommits"
]
}

View File

@ -29,6 +29,8 @@ namespace BSLManager
public void Run()
{
Application.UseSystemConsole = true;
Application.Init();
var top = Application.Top;

View File

@ -2,15 +2,15 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Lamar" Version="5.0.3" />
<PackageReference Include="Lamar" Version="5.0.4" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="5.0.0" />
<PackageReference Include="Terminal.Gui" Version="1.0.0-beta.11" />
<PackageReference Include="Terminal.Gui" Version="1.12.1" />
</ItemGroup>
<ItemGroup>

View File

@ -1,4 +1,5 @@
using System;
using BirdsiteLive.ActivityPub.Models;
using Newtonsoft.Json;
namespace BirdsiteLive.ActivityPub
@ -19,6 +20,8 @@ namespace BirdsiteLive.ActivityPub
if(a.apObject.type == "Follow")
return JsonConvert.DeserializeObject<ActivityUndoFollow>(json);
break;
case "Delete":
return JsonConvert.DeserializeObject<ActivityDelete>(json);
case "Accept":
var accept = JsonConvert.DeserializeObject<ActivityAccept>(json);
//var acceptType = JsonConvert.DeserializeObject<Activity>(accept.apObject);

View File

@ -1,12 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.Text.Json" Version="4.7.2" />
</ItemGroup>

View File

@ -0,0 +1,11 @@
using Newtonsoft.Json;
namespace BirdsiteLive.ActivityPub.Models
{
public class ActivityDelete : Activity
{
public string[] to { get; set; }
[JsonProperty("object")]
public object apObject { get; set; }
}
}

View File

@ -20,6 +20,7 @@ namespace BirdsiteLive.ActivityPub
public string name { get; set; }
public string summary { get; set; }
public string url { get; set; }
public string movedTo { get; set; }
public bool manuallyApprovesFollowers { get; set; }
public string inbox { get; set; }
public bool? discoverable { get; set; } = true;

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
</Project>

View File

@ -5,5 +5,7 @@ namespace BirdsiteLive.Common.Regexes
public class UrlRegexes
{
public static readonly Regex Url = new Regex(@"(.?)(((http|ftp|https):\/\/)[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&amp;:/~\+#]*[\w\-\@?^=%&amp;/~\+#])?)");
public static readonly Regex Domain = new Regex(@"^[a-zA-Z0-9\-_]+(\.[a-zA-Z0-9\-_]+)+$");
}
}

View File

@ -30,5 +30,8 @@
public int MaxStatusFetchAge { get; set; }
public bool EnableQuoteRT { get; set; }
public int FailingFollowerCleanUpThreshold { get; set; } = -1;
public int UserCacheCapacity { get; set; }
}
}

View File

@ -1,13 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Asn1" Version="1.0.9" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Portable.BouncyCastle" Version="1.8.6.7" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Portable.BouncyCastle" Version="1.9.0" />
</ItemGroup>
</Project>

View File

@ -2,6 +2,7 @@
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
@ -11,17 +12,24 @@ using BirdsiteLive.ActivityPub.Models;
using BirdsiteLive.Common.Settings;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Org.BouncyCastle.Bcpg;
namespace BirdsiteLive.Domain
{
public interface IActivityPubService
{
Task<string> GetUserIdAsync(string acct);
Task<Actor> GetUser(string objectId);
Task<HttpStatusCode> PostDataAsync<T>(T data, string targetHost, string actorUrl, string inbox = null);
Task PostNewNoteActivity(Note note, string username, string noteId, string targetHost,
string targetInbox);
Task<WebFingerData> WebFinger(string account);
Task DeleteUserAsync(string username, string targetHost, string targetInbox);
Task<WebFingerData> WebFinger(string account);
}
public class WebFinger
{
public string subject { get; set; }
public string[] aliases { get; set; }
}
public class ActivityPubService : IActivityPubService
@ -41,11 +49,35 @@ namespace BirdsiteLive.Domain
}
#endregion
public async Task<string> GetUserIdAsync(string acct)
{
var splittedAcct = acct.Trim('@').Split('@');
var url = $"https://{splittedAcct[1]}/.well-known/webfinger?resource=acct:{splittedAcct[0]}@{splittedAcct[1]}";
var httpClient = _httpClientFactory.CreateClient("BirdsiteLIVE");
httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
var result = await httpClient.GetAsync(url);
result.EnsureSuccessStatusCode();
var content = await result.Content.ReadAsStringAsync();
var actor = JsonConvert.DeserializeObject<WebFinger>(content);
return actor.aliases.FirstOrDefault();
}
public async Task<Actor> GetUser(string objectId)
{
var httpClient = _httpClientFactory.CreateClient();
var httpClient = _httpClientFactory.CreateClient("BirdsiteLIVE");
httpClient.DefaultRequestHeaders.Add("Accept", "application/activity+json");
var result = await httpClient.GetAsync(objectId);
if (result.StatusCode == HttpStatusCode.Gone)
throw new FollowerIsGoneException();
result.EnsureSuccessStatusCode();
var content = await result.Content.ReadAsStringAsync();
var actor = JsonConvert.DeserializeObject<Actor>(content);
@ -53,6 +85,31 @@ namespace BirdsiteLive.Domain
return actor;
}
public async Task DeleteUserAsync(string username, string targetHost, string targetInbox)
{
try
{
var actor = UrlFactory.GetActorUrl(_instanceSettings.Domain, username);
var deleteUser = new ActivityDelete
{
context = "https://www.w3.org/ns/activitystreams",
id = $"{actor}#delete",
type = "Delete",
actor = actor,
to = new [] { "https://www.w3.org/ns/activitystreams#Public" },
apObject = actor
};
await PostDataAsync(deleteUser, targetHost, actor, targetInbox);
}
catch (Exception e)
{
_logger.LogError(e, "Error deleting {Username} to {Host}{Inbox}", username, targetHost, targetInbox);
throw;
}
}
public async Task PostNewNoteActivity(Note note, string username, string noteId, string targetHost, string targetInbox)
{
try
@ -100,7 +157,7 @@ namespace BirdsiteLive.Domain
var signature = _cryptoService.SignAndGetSignatureHeader(date, actorUrl, targetHost, digest, usedInbox);
var client = _httpClientFactory.CreateClient();
var client = _httpClientFactory.CreateClient("BirdsiteLIVE");
var httpRequestMessage = new HttpRequestMessage
{
Method = HttpMethod.Post,
@ -122,7 +179,7 @@ namespace BirdsiteLive.Domain
public async Task<WebFingerData> WebFinger(string account)
{
var httpClient = _httpClientFactory.CreateClient();
var httpClient = _httpClientFactory.CreateClient("BirdsiteLIVE");
var result = await httpClient.GetAsync("https://" + account.Split('@')[1] + "/.well-known/webfinger?resource=acct:" + account);
var content = await result.Content.ReadAsStringAsync();

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
@ -15,4 +15,8 @@
<ProjectReference Include="..\DataAccessLayers\BirdsiteLive.DAL\BirdsiteLive.DAL.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Enum\" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,51 @@
using System.Linq;
using System.Threading.Tasks;
using BirdsiteLive.DAL.Contracts;
using BirdsiteLive.DAL.Models;
namespace BirdsiteLive.Domain.BusinessUseCases
{
public interface IProcessDeleteUser
{
Task ExecuteAsync(Follower follower);
Task ExecuteAsync(string followerUsername, string followerDomain);
}
public class ProcessDeleteUser : IProcessDeleteUser
{
private readonly IFollowersDal _followersDal;
private readonly ITwitterUserDal _twitterUserDal;
#region Ctor
public ProcessDeleteUser(IFollowersDal followersDal, ITwitterUserDal twitterUserDal)
{
_followersDal = followersDal;
_twitterUserDal = twitterUserDal;
}
#endregion
public async Task ExecuteAsync(string followerUsername, string followerDomain)
{
// Get Follower and Twitter Users
var follower = await _followersDal.GetFollowerAsync(followerUsername, followerDomain);
if (follower == null) return;
await ExecuteAsync(follower);
}
public async Task ExecuteAsync(Follower follower)
{
// Remove twitter users if no more followers
var followings = follower.Followings;
foreach (var following in followings)
{
var followers = await _followersDal.GetFollowersAsync(following);
if (followers.Length == 1 && followers.First().Id == follower.Id)
await _twitterUserDal.DeleteTwitterUserAsync(following);
}
// Remove follower from DB
await _followersDal.DeleteFollowerAsync(follower.Id);
}
}
}

View File

@ -0,0 +1,9 @@
namespace BirdsiteLive.Domain.Enum
{
public enum MigrationTypeEnum
{
Unknown = 0,
Migration = 1,
Deletion = 2
}
}

View File

@ -0,0 +1,8 @@
using System;
namespace BirdsiteLive.Domain
{
public class FollowerIsGoneException : Exception
{
}
}

View File

@ -0,0 +1,352 @@
using System;
using System.Collections.Generic;
using System.Linq;
using BirdsiteLive.Twitter;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using BirdsiteLive.ActivityPub;
using BirdsiteLive.ActivityPub.Models;
using BirdsiteLive.DAL.Contracts;
using BirdsiteLive.ActivityPub.Converters;
using BirdsiteLive.Common.Settings;
using BirdsiteLive.DAL.Models;
using BirdsiteLive.Domain.Enum;
using System.Net.Http;
using BirdsiteLive.Common.Regexes;
using Microsoft.Extensions.Logging;
namespace BirdsiteLive.Domain
{
public class MigrationService
{
private readonly InstanceSettings _instanceSettings;
private readonly ITheFedInfoService _theFedInfoService;
private readonly ITwitterTweetsService _twitterTweetsService;
private readonly IActivityPubService _activityPubService;
private readonly ITwitterUserDal _twitterUserDal;
private readonly IFollowersDal _followersDal;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<MigrationService> _logger;
#region Ctor
public MigrationService(ITwitterTweetsService twitterTweetsService, IActivityPubService activityPubService, ITwitterUserDal twitterUserDal, IFollowersDal followersDal, InstanceSettings instanceSettings, ITheFedInfoService theFedInfoService, IHttpClientFactory httpClientFactory, ILogger<MigrationService> logger)
{
_twitterTweetsService = twitterTweetsService;
_activityPubService = activityPubService;
_twitterUserDal = twitterUserDal;
_followersDal = followersDal;
_instanceSettings = instanceSettings;
_theFedInfoService = theFedInfoService;
_httpClientFactory = httpClientFactory;
_logger = logger;
}
#endregion
public string GetMigrationCode(string acct)
{
var hash = GetHashString(acct);
return $"[[BirdsiteLIVE-MigrationCode|{hash.Substring(0, 10)}]]";
}
public string GetDeletionCode(string acct)
{
var hash = GetHashString(acct);
return $"[[BirdsiteLIVE-DeletionCode|{hash.Substring(0, 10)}]]";
}
public bool ValidateTweet(string acct, string tweetId, MigrationTypeEnum type)
{
string code;
if (type == MigrationTypeEnum.Migration)
code = GetMigrationCode(acct);
else if (type == MigrationTypeEnum.Deletion)
code = GetDeletionCode(acct);
else
throw new NotImplementedException();
var castedTweetId = ExtractedTweetId(tweetId);
var tweet = _twitterTweetsService.GetTweet(castedTweetId);
if (tweet == null)
throw new Exception("Tweet not found");
if (tweet.CreatorName.Trim().ToLowerInvariant() != acct.Trim().ToLowerInvariant())
throw new Exception($"Tweet not published by @{acct}");
if (!tweet.MessageContent.Contains(code))
{
var message = "Tweet don't have migration code";
if (type == MigrationTypeEnum.Deletion)
message = "Tweet don't have deletion code";
throw new Exception(message);
}
return true;
}
private long ExtractedTweetId(string tweetId)
{
if (string.IsNullOrWhiteSpace(tweetId))
throw new ArgumentException("No provided Tweet ID");
long castedId;
if (long.TryParse(tweetId, out castedId))
return castedId;
var urlPart = tweetId.Split('/').LastOrDefault();
if (long.TryParse(urlPart, out castedId))
return castedId;
throw new ArgumentException("Unvalid Tweet ID");
}
public async Task<ValidatedFediverseUser> ValidateFediverseAcctAsync(string fediverseAcct)
{
if (string.IsNullOrWhiteSpace(fediverseAcct))
throw new ArgumentException("Please provide Fediverse account");
if (!fediverseAcct.Contains('@') || !fediverseAcct.StartsWith("@") || fediverseAcct.Trim('@').Split('@').Length != 2)
throw new ArgumentException("Please provide valid Fediverse handle");
var objectId = await _activityPubService.GetUserIdAsync(fediverseAcct);
var user = await _activityPubService.GetUser(objectId);
var result = new ValidatedFediverseUser
{
FediverseAcct = fediverseAcct,
ObjectId = objectId,
User = user,
IsValid = user != null
};
return result;
}
public async Task MigrateAccountAsync(ValidatedFediverseUser validatedUser, string acct)
{
// Apply moved to
var twitterAccount = await _twitterUserDal.GetTwitterUserAsync(acct);
if (twitterAccount == null)
{
await _twitterUserDal.CreateTwitterUserAsync(acct, -1, validatedUser.ObjectId, validatedUser.FediverseAcct);
twitterAccount = await _twitterUserDal.GetTwitterUserAsync(acct);
}
twitterAccount.MovedTo = validatedUser.User.id;
twitterAccount.MovedToAcct = validatedUser.FediverseAcct;
twitterAccount.LastSync = DateTime.UtcNow;
await _twitterUserDal.UpdateTwitterUserAsync(twitterAccount);
// Notify Followers
var message = $@"<p>[BSL MIRROR SERVICE NOTIFICATION]<br/>This bot has been disabled by its original owner.<br/>It has been redirected to {validatedUser.FediverseAcct}.</p>";
NotifyFollowers(acct, twitterAccount, message);
}
private void NotifyFollowers(string acct, SyncTwitterUser twitterAccount, string message)
{
var t = Task.Run(async () =>
{
var followers = await _followersDal.GetFollowersAsync(twitterAccount.Id);
foreach (var follower in followers)
{
try
{
var noteId = Guid.NewGuid().ToString();
var actorUrl = UrlFactory.GetActorUrl(_instanceSettings.Domain, acct);
var noteUrl = UrlFactory.GetNoteUrl(_instanceSettings.Domain, acct, noteId);
//var to = validatedUser.ObjectId;
var to = follower.ActorId;
var cc = new string[0];
var note = new Note
{
id = noteUrl,
published = DateTime.UtcNow.ToString("s") + "Z",
url = noteUrl,
attributedTo = actorUrl,
to = new[] { to },
cc = cc,
content = message,
tag = new Tag[]{
new Tag()
{
type = "Mention",
href = follower.ActorId,
name = $"@{follower.Acct}@{follower.Host}"
}
},
};
if (!string.IsNullOrWhiteSpace(follower.SharedInboxRoute))
await _activityPubService.PostNewNoteActivity(note, acct, noteId, follower.Host, follower.SharedInboxRoute);
else
await _activityPubService.PostNewNoteActivity(note, acct, noteId, follower.Host, follower.InboxRoute);
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
}
}
});
}
public async Task DeleteAccountAsync(string acct)
{
// Apply deleted state
var twitterAccount = await _twitterUserDal.GetTwitterUserAsync(acct);
if (twitterAccount == null)
{
await _twitterUserDal.CreateTwitterUserAsync(acct, -1);
twitterAccount = await _twitterUserDal.GetTwitterUserAsync(acct);
}
twitterAccount.Deleted = true;
twitterAccount.LastSync = DateTime.UtcNow;
await _twitterUserDal.UpdateTwitterUserAsync(twitterAccount);
// Notify Followers
var message = $@"<p>[BSL MIRROR SERVICE NOTIFICATION]<br/>This bot has been deleted by its original owner.<br/></p>";
NotifyFollowers(acct, twitterAccount, message);
// Delete remote accounts
DeleteRemoteAccounts(acct);
}
private void DeleteRemoteAccounts(string acct)
{
var t = Task.Run(async () =>
{
var allUsers = await _followersDal.GetAllFollowersAsync();
var followersWtSharedInbox = allUsers
.Where(x => !string.IsNullOrWhiteSpace(x.SharedInboxRoute))
.GroupBy(x => x.Host)
.ToList();
foreach (var followerGroup in followersWtSharedInbox)
{
var host = followerGroup.First().Host;
var sharedInbox = followerGroup.First().SharedInboxRoute;
var t1 = Task.Run(async () =>
{
try
{
await _activityPubService.DeleteUserAsync(acct, host, sharedInbox);
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
}
});
}
var followerWtInbox = allUsers
.Where(x => !string.IsNullOrWhiteSpace(x.SharedInboxRoute))
.ToList();
foreach (var followerGroup in followerWtInbox)
{
var host = followerGroup.Host;
var sharedInbox = followerGroup.InboxRoute;
var t1 = Task.Run(async () =>
{
try
{
await _activityPubService.DeleteUserAsync(acct, host, sharedInbox);
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
}
});
}
});
}
public async Task TriggerRemoteMigrationAsync(string id, string tweetIdStg, string handle)
{
var url = $"https://{{0}}/migration/move/{{1}}/{{2}}/{handle}";
await ProcessRemoteMigrationAsync(id, tweetIdStg, url);
}
public async Task TriggerRemoteDeleteAsync(string id, string tweetIdStg)
{
var url = $"https://{{0}}/migration/delete/{{1}}/{{2}}";
await ProcessRemoteMigrationAsync(id, tweetIdStg, url);
}
private async Task ProcessRemoteMigrationAsync(string id, string tweetIdStg, string urlPattern)
{
try
{
var instances = await RetrieveCompatibleBslInstancesAsync();
var tweetId = ExtractedTweetId(tweetIdStg);
foreach (var instance in instances)
{
try
{
var host = instance.Host;
if(!UrlRegexes.Domain.IsMatch(host)) continue;
var url = string.Format(urlPattern, host, id, tweetId);
var client = _httpClientFactory.CreateClient("BirdsiteLIVE");
var result = await client.PostAsync(url, null);
result.EnsureSuccessStatusCode();
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
}
}
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
}
}
private async Task<List<BslInstanceInfo>> RetrieveCompatibleBslInstancesAsync()
{
var instances = await _theFedInfoService.GetBslInstanceListAsync();
var filteredInstances = instances
.Where(x => x.Version >= new Version(0, 21, 0))
.Where(x => string.Compare(x.Host,
_instanceSettings.Domain,
StringComparison.InvariantCultureIgnoreCase) != 0)
.ToList();
return filteredInstances;
}
private byte[] GetHash(string inputString)
{
using (HashAlgorithm algorithm = SHA256.Create())
return algorithm.ComputeHash(Encoding.UTF8.GetBytes(inputString));
}
private string GetHashString(string inputString)
{
StringBuilder sb = new StringBuilder();
foreach (byte b in GetHash(inputString))
sb.Append(b.ToString("X2"));
return sb.ToString();
}
}
public class ValidatedFediverseUser
{
public string FediverseAcct { get; set; }
public string ObjectId { get; set; }
public Actor User { get; set; }
public bool IsValid { get; set; }
}
}

View File

@ -0,0 +1,162 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace BirdsiteLive.Domain
{
public interface ITheFedInfoService
{
Task<List<BslInstanceInfo>> GetBslInstanceListAsync();
}
public class TheFedInfoService : ITheFedInfoService
{
private readonly IHttpClientFactory _httpClientFactory;
#region Ctor
public TheFedInfoService(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
#endregion
public async Task<List<BslInstanceInfo>> GetBslInstanceListAsync()
{
var cancellationToken = CancellationToken.None;
var result = await CallGraphQlAsync<MyResponseData>(
new Uri("https://the-federation.info/graphql"),
HttpMethod.Get,
"query ($platform: String!) { nodes(platform: $platform) { host, version } }",
new
{
platform = "birdsitelive",
},
cancellationToken);
var convertedResults = ConvertResults(result);
return convertedResults;
}
private List<BslInstanceInfo> ConvertResults(GraphQLResponse<MyResponseData> qlData)
{
var results = new List<BslInstanceInfo>();
foreach (var instanceInfo in qlData.Data.Nodes)
{
try
{
var rawVersion = instanceInfo.Version.Split('+').First();
if (string.IsNullOrWhiteSpace(rawVersion)) continue;
var version = Version.Parse(rawVersion);
if(version <= new Version(0,1,0)) continue;
var instance = new BslInstanceInfo
{
Host = instanceInfo.Host,
Version = version
};
results.Add(instance);
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
return results;
}
private async Task<GraphQLResponse<TResponse>> CallGraphQlAsync<TResponse>(Uri endpoint, HttpMethod method, string query, object variables, CancellationToken cancellationToken)
{
var content = new StringContent(SerializeGraphQlCall(query, variables), Encoding.UTF8, "application/json");
var httpRequestMessage = new HttpRequestMessage
{
Method = method,
Content = content,
RequestUri = endpoint,
};
//add authorization headers if necessary here
httpRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var httpClient = _httpClientFactory.CreateClient("BirdsiteLIVE");
using (var response = await httpClient.SendAsync(httpRequestMessage, cancellationToken))
{
//if (response.IsSuccessStatusCode)
if (response?.Content.Headers.ContentType?.MediaType == "application/json")
{
var responseString = await response.Content.ReadAsStringAsync(); //cancellationToken supported for .NET 5/6
return DeserializeGraphQlCall<TResponse>(responseString);
}
else
{
throw new ApplicationException($"Unable to contact '{endpoint}': {response.StatusCode} - {response.ReasonPhrase}");
}
}
}
private string SerializeGraphQlCall(string query, object variables)
{
var sb = new StringBuilder();
var textWriter = new StringWriter(sb);
var serializer = new JsonSerializer();
serializer.Serialize(textWriter, new
{
query = query,
variables = variables,
});
return sb.ToString();
}
private GraphQLResponse<TResponse> DeserializeGraphQlCall<TResponse>(string response)
{
var serializer = new JsonSerializer();
var stringReader = new StringReader(response);
var jsonReader = new JsonTextReader(stringReader);
var result = serializer.Deserialize<GraphQLResponse<TResponse>>(jsonReader);
return result;
}
private class GraphQLResponse<TResponse>
{
public List<GraphQLError> Errors { get; set; }
public TResponse Data { get; set; }
}
private class GraphQLError
{
public string Message { get; set; }
public List<GraphQLErrorLocation> Locations { get; set; }
public List<object> Path { get; set; } //either int or string
}
private class GraphQLErrorLocation
{
public int Line { get; set; }
public int Column { get; set; }
}
private class MyResponseData
{
public Node[] Nodes { get; set; }
}
private class Node
{
public string Host { get; set; }
public string Version { get; set; }
}
}
public class BslInstanceInfo
{
public string Host { get; set; }
public Version Version { get; set; }
}
}

View File

@ -0,0 +1,22 @@
using System.Linq;
namespace BirdsiteLive.Domain.Tools
{
public class SigValidationResultExtractor
{
public static string GetUserName(SignatureValidationResult result)
{
return result.User.preferredUsername.ToLowerInvariant().Trim();
}
public static string GetHost(SignatureValidationResult result)
{
return result.User.url.Replace("https://", string.Empty).Split('/').First();
}
public static string GetSharedInbox(SignatureValidationResult result)
{
return result.User?.endpoints?.sharedInbox;
}
}
}

View File

@ -12,6 +12,7 @@ using BirdsiteLive.Common.Regexes;
using BirdsiteLive.Common.Settings;
using BirdsiteLive.Cryptography;
using BirdsiteLive.DAL.Contracts;
using BirdsiteLive.DAL.Models;
using BirdsiteLive.Domain.BusinessUseCases;
using BirdsiteLive.Domain.Repository;
using BirdsiteLive.Domain.Statistics;
@ -25,15 +26,17 @@ namespace BirdsiteLive.Domain
{
public interface IUserService
{
Actor GetUser(TwitterUser twitterUser);
Actor GetUser(TwitterUser twitterUser, SyncTwitterUser dbTwitterUser);
Task<bool> FollowRequestedAsync(string signature, string method, string path, string queryString, Dictionary<string, string> requestHeaders, ActivityFollow activity, string body);
Task<bool> UndoFollowRequestedAsync(string signature, string method, string path, string queryString, Dictionary<string, string> requestHeaders, ActivityUndoFollow activity, string body);
Task<bool> SendRejectFollowAsync(ActivityFollow activity, string followerHost);
Task<bool> DeleteRequestedAsync(string signature, string method, string path, string queryString, Dictionary<string, string> requestHeaders, ActivityDelete activity, string body);
}
public class UserService : IUserService
{
private readonly IProcessDeleteUser _processDeleteUser;
private readonly IProcessFollowUser _processFollowUser;
private readonly IProcessUndoFollowUser _processUndoFollowUser;
@ -50,7 +53,7 @@ namespace BirdsiteLive.Domain
private readonly IFollowersDal _followerDal;
#region Ctor
public UserService(InstanceSettings instanceSettings, ICryptoService cryptoService, IActivityPubService activityPubService, IProcessFollowUser processFollowUser, IProcessUndoFollowUser processUndoFollowUser, IStatusExtractor statusExtractor, IExtractionStatisticsHandler statisticsHandler, ITwitterUserService twitterUserService, IModerationRepository moderationRepository, IFollowersDal followerDal)
public UserService(InstanceSettings instanceSettings, ICryptoService cryptoService, IActivityPubService activityPubService, IProcessFollowUser processFollowUser, IProcessUndoFollowUser processUndoFollowUser, IStatusExtractor statusExtractor, IExtractionStatisticsHandler statisticsHandler, ITwitterUserService twitterUserService, IModerationRepository moderationRepository, IFollowersDal followerDal, IProcessDeleteUser processDeleteUser)
{
_instanceSettings = instanceSettings;
_cryptoService = cryptoService;
@ -62,10 +65,11 @@ namespace BirdsiteLive.Domain
_twitterUserService = twitterUserService;
_moderationRepository = moderationRepository;
_followerDal = followerDal;
_processDeleteUser = processDeleteUser;
}
#endregion
public Actor GetUser(TwitterUser twitterUser)
public Actor GetUser(TwitterUser twitterUser, SyncTwitterUser dbTwitterUser)
{
var actorUrl = UrlFactory.GetActorUrl(_instanceSettings.Domain, twitterUser.Acct);
var acct = twitterUser.Acct.ToLowerInvariant();
@ -116,9 +120,10 @@ namespace BirdsiteLive.Domain
preferredUsername = acct,
name = twitterUser.Name,
inbox = $"{actorUrl}/inbox",
summary = description,
summary = "[UNOFFICIAL MIRROR: This is a view of Twitter using ActivityPub]<br/><br/>" + description,
url = actorUrl,
manuallyApprovesFollowers = twitterUser.Protected,
discoverable = false,
publicKey = new PublicKey()
{
id = $"{actorUrl}#main-key",
@ -135,11 +140,32 @@ namespace BirdsiteLive.Domain
mediaType = "image/jpeg",
url = twitterUser.ProfileBannerURL
},
attachment = attachments.ToArray(),
attachment = new []
{
new UserAttachment
{
type = "PropertyValue",
name = "Official Account",
value = $"<a href=\"https://twitter.com/{acct}\" rel=\"me nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"ellipsis\">twitter.com/{acct}</span></a>"
},
new UserAttachment
{
type = "PropertyValue",
name = "Disclaimer",
value = "This is an automatically created and managed mirror profile from Twitter. While it reflects exactly the content of the original account, it doesn't provide support for interactions and replies. It is an equivalent view from other 3rd party Twitter client apps and uses the same technical means to provide it."
},
new UserAttachment
{
type = "PropertyValue",
name = "Take control of this account",
value = $"<a href=\"https://{_instanceSettings.Domain}/migration/move/{acct}\" rel=\"me nofollow noopener noreferrer\" target=\"_blank\">MANAGE</a>"
}
},
endpoints = new EndPoints
{
sharedInbox = $"https://{_instanceSettings.Domain}/inbox"
}
},
movedTo = dbTwitterUser?.MovedTo
};
if (twitterUser.Verified)
@ -172,10 +198,10 @@ namespace BirdsiteLive.Domain
if (!sigValidation.SignatureIsValidated) return false;
// Prepare data
var followerUserName = sigValidation.User.preferredUsername.ToLowerInvariant().Trim();
var followerHost = sigValidation.User.url.Replace("https://", string.Empty).Split('/').First();
var followerUserName = SigValidationResultExtractor.GetUserName(sigValidation);
var followerHost = SigValidationResultExtractor.GetHost(sigValidation);
var followerInbox = sigValidation.User.inbox;
var followerSharedInbox = sigValidation.User?.endpoints?.sharedInbox;
var followerSharedInbox = SigValidationResultExtractor.GetSharedInbox(sigValidation);
var twitterUser = activity.apObject.Split('/').Last().Replace("@", string.Empty).ToLowerInvariant().Trim();
// Make sure to only keep routes
@ -269,7 +295,7 @@ namespace BirdsiteLive.Domain
return result == HttpStatusCode.Accepted ||
result == HttpStatusCode.OK; //TODO: revamp this for better error handling
}
private string OnlyKeepRoute(string inbox, string host)
{
if (string.IsNullOrWhiteSpace(inbox))
@ -314,6 +340,22 @@ namespace BirdsiteLive.Domain
return result == HttpStatusCode.Accepted || result == HttpStatusCode.OK; //TODO: revamp this for better error handling
}
public async Task<bool> DeleteRequestedAsync(string signature, string method, string path, string queryString, Dictionary<string, string> requestHeaders,
ActivityDelete activity, string body)
{
// Validate
var sigValidation = await ValidateSignature(activity.actor, signature, method, path, queryString, requestHeaders, body);
if (!sigValidation.SignatureIsValidated) return false;
// Remove user and followings
var followerUserName = SigValidationResultExtractor.GetUserName(sigValidation);
var followerHost = SigValidationResultExtractor.GetHost(sigValidation);
await _processDeleteUser.ExecuteAsync(followerUserName, followerHost);
return true;
}
private async Task<SignatureValidationResult> ValidateSignature(string actor, string rawSig, string method, string path, string queryString, Dictionary<string, string> requestHeaders, string body)
{
//Check Date Validity

View File

@ -1,12 +1,6 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using BirdsiteLive.ActivityPub;
using BirdsiteLive.ActivityPub.Converters;
using BirdsiteLive.Common.Settings;
using BirdsiteLive.DAL.Contracts;
using System.Threading.Tasks;
using BirdsiteLive.DAL.Models;
using BirdsiteLive.Domain;
using BirdsiteLive.Domain.BusinessUseCases;
namespace BirdsiteLive.Moderation.Actions
{
@ -17,16 +11,14 @@ namespace BirdsiteLive.Moderation.Actions
public class RemoveFollowerAction : IRemoveFollowerAction
{
private readonly IFollowersDal _followersDal;
private readonly ITwitterUserDal _twitterUserDal;
private readonly IRejectAllFollowingsAction _rejectAllFollowingsAction;
private readonly IProcessDeleteUser _processDeleteUser;
#region Ctor
public RemoveFollowerAction(IFollowersDal followersDal, ITwitterUserDal twitterUserDal, IRejectAllFollowingsAction rejectAllFollowingsAction)
public RemoveFollowerAction(IRejectAllFollowingsAction rejectAllFollowingsAction, IProcessDeleteUser processDeleteUser)
{
_followersDal = followersDal;
_twitterUserDal = twitterUserDal;
_rejectAllFollowingsAction = rejectAllFollowingsAction;
_processDeleteUser = processDeleteUser;
}
#endregion
@ -36,16 +28,7 @@ namespace BirdsiteLive.Moderation.Actions
await _rejectAllFollowingsAction.ProcessAsync(follower);
// Remove twitter users if no more followers
var followings = follower.Followings;
foreach (var following in followings)
{
var followers = await _followersDal.GetFollowersAsync(following);
if (followers.Length == 1 && followers.First().Id == follower.Id)
await _twitterUserDal.DeleteTwitterUserAsync(following);
}
// Remove follower from DB
await _followersDal.DeleteFollowerAsync(follower.Id);
await _processDeleteUser.ExecuteAsync(follower);
}
}
}

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>

View File

@ -29,7 +29,7 @@ namespace BirdsiteLive.Moderation.Processors
{
if (type == ModerationTypeEnum.None) return;
var twitterUsers = await _twitterUserDal.GetAllTwitterUsersAsync();
var twitterUsers = await _twitterUserDal.GetAllTwitterUsersAsync(false);
foreach (var user in twitterUsers)
{

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<LangVersion>latest</LangVersion>
</PropertyGroup>

View File

@ -9,6 +9,7 @@ using BirdsiteLive.Moderation.Actions;
using BirdsiteLive.Pipeline.Contracts;
using BirdsiteLive.Pipeline.Models;
using BirdsiteLive.Twitter;
using BirdsiteLive.Twitter.Models;
namespace BirdsiteLive.Pipeline.Processors
{
@ -35,26 +36,61 @@ namespace BirdsiteLive.Pipeline.Processors
foreach (var user in syncTwitterUsers)
{
var userView = _twitterUserService.GetUser(user.Acct);
if (userView == null)
{
await AnalyseFailingUserAsync(user);
}
else if (!userView.Protected)
{
user.FetchingErrorCount = 0;
var userWtData = new UserWithDataToSync
{
User = user
};
usersWtData.Add(userWtData);
}
}
TwitterUser userView = null;
try
{
userView = _twitterUserService.GetUser(user.Acct);
}
catch (UserNotFoundException)
{
await ProcessNotFoundUserAsync(user);
continue;
}
catch (UserHasBeenSuspendedException)
{
await ProcessNotFoundUserAsync(user);
continue;
}
catch (RateLimitExceededException)
{
await ProcessRateLimitExceededAsync(user);
continue;
}
catch (Exception)
{
// ignored
}
if (userView == null || userView.Protected)
{
await ProcessFailingUserAsync(user);
continue;
}
user.FetchingErrorCount = 0;
var userWtData = new UserWithDataToSync
{
User = user
};
usersWtData.Add(userWtData);
}
return usersWtData.ToArray();
}
private async Task AnalyseFailingUserAsync(SyncTwitterUser user)
private async Task ProcessRateLimitExceededAsync(SyncTwitterUser user)
{
var dbUser = await _twitterUserDal.GetTwitterUserAsync(user.Acct);
dbUser.LastSync = DateTime.UtcNow;
await _twitterUserDal.UpdateTwitterUserAsync(dbUser);
}
private async Task ProcessNotFoundUserAsync(SyncTwitterUser user)
{
await _removeTwitterAccountAction.ProcessAsync(user);
}
private async Task ProcessFailingUserAsync(SyncTwitterUser user)
{
var dbUser = await _twitterUserDal.GetTwitterUserAsync(user.Acct);
dbUser.FetchingErrorCount++;
@ -68,9 +104,6 @@ namespace BirdsiteLive.Pipeline.Processors
{
await _twitterUserDal.UpdateTwitterUserAsync(dbUser);
}
// Purge
_twitterUserService.PurgeUser(user.Acct);
}
}
}

View File

@ -49,12 +49,12 @@ namespace BirdsiteLive.Pipeline.Processors
{
var tweetId = tweets.Last().Id;
var now = DateTime.UtcNow;
await _twitterUserDal.UpdateTwitterUserAsync(user.Id, tweetId, tweetId, user.FetchingErrorCount, now);
await _twitterUserDal.UpdateTwitterUserAsync(user.Id, tweetId, tweetId, user.FetchingErrorCount, now, user.MovedTo, user.MovedToAcct, user.Deleted);
}
else
{
var now = DateTime.UtcNow;
await _twitterUserDal.UpdateTwitterUserAsync(user.Id, user.LastTweetPostedId, user.LastTweetSynchronizedForAllFollowersId, user.FetchingErrorCount, now);
await _twitterUserDal.UpdateTwitterUserAsync(user.Id, user.LastTweetPostedId, user.LastTweetSynchronizedForAllFollowersId, user.FetchingErrorCount, now, user.MovedTo, user.MovedToAcct, user.Deleted);
}
}

View File

@ -39,7 +39,7 @@ namespace BirdsiteLive.Pipeline.Processors
try
{
var maxUsersNumber = await _maxUsersNumberProvider.GetMaxUsersNumberAsync();
var users = await _twitterUserDal.GetAllTwitterUsersAsync(maxUsersNumber);
var users = await _twitterUserDal.GetAllTwitterUsersAsync(maxUsersNumber, false);
var userCount = users.Any() ? users.Length : 1;
var splitNumber = (int) Math.Ceiling(userCount / 15d);

View File

@ -3,6 +3,8 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BirdsiteLive.DAL.Contracts;
using BirdsiteLive.DAL.Models;
using BirdsiteLive.Moderation.Actions;
using BirdsiteLive.Pipeline.Contracts;
using BirdsiteLive.Pipeline.Models;
using Microsoft.Extensions.Logging;
@ -13,12 +15,14 @@ namespace BirdsiteLive.Pipeline.Processors
{
private readonly ITwitterUserDal _twitterUserDal;
private readonly ILogger<SaveProgressionProcessor> _logger;
private readonly IRemoveTwitterAccountAction _removeTwitterAccountAction;
#region Ctor
public SaveProgressionProcessor(ITwitterUserDal twitterUserDal, ILogger<SaveProgressionProcessor> logger)
public SaveProgressionProcessor(ITwitterUserDal twitterUserDal, ILogger<SaveProgressionProcessor> logger, IRemoveTwitterAccountAction removeTwitterAccountAction)
{
_twitterUserDal = twitterUserDal;
_logger = logger;
_removeTwitterAccountAction = removeTwitterAccountAction;
}
#endregion
@ -28,28 +32,23 @@ namespace BirdsiteLive.Pipeline.Processors
{
if (userWithTweetsToSync.Tweets.Length == 0)
{
_logger.LogWarning("No tweets synchronized");
_logger.LogInformation("No tweets synchronized");
await UpdateUserSyncDateAsync(userWithTweetsToSync.User);
return;
}
if(userWithTweetsToSync.Followers.Length == 0)
{
_logger.LogWarning("No Followers found for {User}", userWithTweetsToSync.User.Acct);
_logger.LogInformation("No Followers found for {User}", userWithTweetsToSync.User.Acct);
await _removeTwitterAccountAction.ProcessAsync(userWithTweetsToSync.User);
return;
}
var userId = userWithTweetsToSync.User.Id;
var followingSyncStatuses = userWithTweetsToSync.Followers.Select(x => x.FollowingsSyncStatus[userId]).ToList();
if (followingSyncStatuses.Count == 0)
{
_logger.LogWarning("No Followers sync found for {User}, Id: {UserId}", userWithTweetsToSync.User.Acct, userId);
return;
}
var lastPostedTweet = userWithTweetsToSync.Tweets.Select(x => x.Id).Max();
var minimumSync = followingSyncStatuses.Min();
var now = DateTime.UtcNow;
await _twitterUserDal.UpdateTwitterUserAsync(userId, lastPostedTweet, minimumSync, userWithTweetsToSync.User.FetchingErrorCount, now);
await _twitterUserDal.UpdateTwitterUserAsync(userId, lastPostedTweet, minimumSync, userWithTweetsToSync.User.FetchingErrorCount, now, userWithTweetsToSync.User.MovedTo, userWithTweetsToSync.User.MovedToAcct, userWithTweetsToSync.User.Deleted);
}
catch (Exception e)
{
@ -57,5 +56,11 @@ namespace BirdsiteLive.Pipeline.Processors
throw;
}
}
private async Task UpdateUserSyncDateAsync(SyncTwitterUser user)
{
user.LastSync = DateTime.UtcNow;
await _twitterUserDal.UpdateTwitterUserAsync(user);
}
}
}

View File

@ -5,9 +5,11 @@ using System.Net;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using BirdsiteLive.Common.Settings;
using BirdsiteLive.DAL.Contracts;
using BirdsiteLive.DAL.Models;
using BirdsiteLive.Domain;
using BirdsiteLive.Moderation.Actions;
using BirdsiteLive.Pipeline.Contracts;
using BirdsiteLive.Pipeline.Models;
using BirdsiteLive.Pipeline.Processors.SubTasks;
@ -23,14 +25,18 @@ namespace BirdsiteLive.Pipeline.Processors
private readonly ISendTweetsToInboxTask _sendTweetsToInboxTask;
private readonly ISendTweetsToSharedInboxTask _sendTweetsToSharedInbox;
private readonly IFollowersDal _followersDal;
private readonly InstanceSettings _instanceSettings;
private readonly ILogger<SendTweetsToFollowersProcessor> _logger;
private readonly IRemoveFollowerAction _removeFollowerAction;
#region Ctor
public SendTweetsToFollowersProcessor(ISendTweetsToInboxTask sendTweetsToInboxTask, ISendTweetsToSharedInboxTask sendTweetsToSharedInbox, IFollowersDal followersDal, ILogger<SendTweetsToFollowersProcessor> logger)
public SendTweetsToFollowersProcessor(ISendTweetsToInboxTask sendTweetsToInboxTask, ISendTweetsToSharedInboxTask sendTweetsToSharedInbox, IFollowersDal followersDal, ILogger<SendTweetsToFollowersProcessor> logger, InstanceSettings instanceSettings, IRemoveFollowerAction removeFollowerAction)
{
_sendTweetsToInboxTask = sendTweetsToInboxTask;
_sendTweetsToSharedInbox = sendTweetsToSharedInbox;
_logger = logger;
_instanceSettings = instanceSettings;
_removeFollowerAction = removeFollowerAction;
_followersDal = followersDal;
}
#endregion
@ -107,7 +113,17 @@ namespace BirdsiteLive.Pipeline.Processors
private async Task ProcessFailingUserAsync(Follower follower)
{
follower.PostingErrorCount++;
await _followersDal.UpdateFollowerAsync(follower);
if (follower.PostingErrorCount > _instanceSettings.FailingFollowerCleanUpThreshold
&& _instanceSettings.FailingFollowerCleanUpThreshold > 0
|| follower.PostingErrorCount > 2147483600)
{
await _removeFollowerAction.ProcessAsync(follower);
}
else
{
await _followersDal.UpdateFollowerAsync(follower);
}
}
}
}

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>

View File

@ -1,4 +1,5 @@
using System;
using BirdsiteLive.Common.Settings;
using BirdsiteLive.Twitter.Models;
using Microsoft.Extensions.Caching.Memory;
@ -13,11 +14,8 @@ namespace BirdsiteLive.Twitter
{
private readonly ITwitterUserService _twitterService;
private MemoryCache _userCache = new MemoryCache(new MemoryCacheOptions()
{
SizeLimit = 5000
});
private MemoryCacheEntryOptions _cacheEntryOptions = new MemoryCacheEntryOptions()
private readonly MemoryCache _userCache;
private readonly MemoryCacheEntryOptions _cacheEntryOptions = new MemoryCacheEntryOptions()
.SetSize(1)//Size amount
//Priority on removing when reaching size limit (memory pressure)
.SetPriority(CacheItemPriority.High)
@ -27,9 +25,14 @@ namespace BirdsiteLive.Twitter
.SetAbsoluteExpiration(TimeSpan.FromDays(7));
#region Ctor
public CachedTwitterUserService(ITwitterUserService twitterService)
public CachedTwitterUserService(ITwitterUserService twitterService, InstanceSettings settings)
{
_twitterService = twitterService;
_userCache = new MemoryCache(new MemoryCacheOptions()
{
SizeLimit = settings.UserCacheCapacity
});
}
#endregion
@ -44,6 +47,11 @@ namespace BirdsiteLive.Twitter
return user;
}
public bool IsUserApiRateLimited()
{
return _twitterService.IsUserApiRateLimited();
}
public void PurgeUser(string username)
{
_userCache.Remove(username);

View File

@ -0,0 +1,9 @@
using System;
namespace BirdsiteLive.Twitter
{
public class RateLimitExceededException : Exception
{
}
}

View File

@ -0,0 +1,9 @@
using System;
namespace BirdsiteLive.Twitter
{
public class UserHasBeenSuspendedException : Exception
{
}
}

View File

@ -0,0 +1,9 @@
using System;
namespace BirdsiteLive.Twitter
{
public class UserNotFoundException : Exception
{
}
}

View File

@ -40,7 +40,8 @@ namespace BirdsiteLive.Twitter.Extractors
IsRetweet = tweet.IsRetweet || tweet.QuotedStatusId != null,
RetweetUrl = ExtractRetweetUrl(tweet),
IsSensitive = tweet.PossiblySensitive,
QuoteTweetUrl = tweet.QuotedStatusId != null ? "https://" + _instanceSettings.Domain + "/users/" + tweet.QuotedTweet.CreatedBy.ScreenName + "/statuses/" + tweet.QuotedStatusId : null
QuoteTweetUrl = tweet.QuotedStatusId != null ? "https://" + _instanceSettings.Domain + "/users/" + tweet.QuotedTweet.CreatedBy.ScreenName + "/statuses/" + tweet.QuotedStatusId : null,
CreatorName = tweet.CreatedBy.UserIdentifier.ScreenName
};
return extractedTweet;

View File

@ -17,5 +17,6 @@ namespace BirdsiteLive.Twitter.Models
public string RetweetUrl { get; set; }
public bool IsSensitive { get; set; }
public string QuoteTweetUrl { get; set; }
public string CreatorName { get; set; }
}
}

View File

@ -13,6 +13,8 @@ namespace BirdsiteLive.Statistics.Domain
void CalledTweetApi();
void CalledTimelineApi();
ApiStatistics GetStatistics();
int GetCurrentUserCalls();
}
//Rate limits: https://developer.twitter.com/en/docs/twitter-api/v1/rate-limits
@ -60,7 +62,12 @@ namespace BirdsiteLive.Statistics.Domain
foreach (var old in oldSnapshots) _snapshots.TryRemove(old, out var data);
}
public void CalledUserApi() //GET users/show - 900/15mins
public int GetCurrentUserCalls()
{
return _userCalls;
}
public void CalledUserApi() //GET users/show - 300/15mins
{
Interlocked.Increment(ref _userCalls);
}

View File

@ -6,6 +6,7 @@ using BirdsiteLive.Twitter.Models;
using BirdsiteLive.Twitter.Tools;
using Microsoft.Extensions.Logging;
using Tweetinvi;
using Tweetinvi.Exceptions;
using Tweetinvi.Models;
namespace BirdsiteLive.Twitter
@ -13,6 +14,7 @@ namespace BirdsiteLive.Twitter
public interface ITwitterUserService
{
TwitterUser GetUser(string username);
bool IsUserApiRateLimited();
}
public class TwitterUserService : ITwitterUserService
@ -32,27 +34,46 @@ namespace BirdsiteLive.Twitter
public TwitterUser GetUser(string username)
{
//Check if API is saturated
if (IsUserApiRateLimited()) throw new RateLimitExceededException();
//Proceed to account retrieval
_twitterAuthenticationInitializer.EnsureAuthenticationIsInitialized();
ExceptionHandler.SwallowWebExceptions = false;
RateLimit.RateLimitTrackerMode = RateLimitTrackerMode.TrackOnly;
IUser user;
try
{
user = User.GetUserFromScreenName(username);
_statisticsHandler.CalledUserApi();
if (user == null)
}
catch (TwitterException e)
{
if (e.TwitterExceptionInfos.Any(x => x.Message.ToLowerInvariant().Contains("User has been suspended".ToLowerInvariant())))
{
_logger.LogWarning("User {username} not found", username);
return null;
throw new UserHasBeenSuspendedException();
}
else if (e.TwitterExceptionInfos.Any(x => x.Message.ToLowerInvariant().Contains("User not found".ToLowerInvariant())))
{
throw new UserNotFoundException();
}
else if (e.TwitterExceptionInfos.Any(x => x.Message.ToLowerInvariant().Contains("Rate limit exceeded".ToLowerInvariant())))
{
throw new RateLimitExceededException();
}
else
{
throw;
}
}
catch (Exception e)
{
_logger.LogError(e, "Error retrieving user {Username}", username);
// TODO keep track of error, see where to remove user if too much errors
return null;
throw;
}
finally
{
_statisticsHandler.CalledUserApi();
}
// Expand URLs
@ -74,5 +95,32 @@ namespace BirdsiteLive.Twitter
Verified = user.Verified
};
}
public bool IsUserApiRateLimited()
{
// Retrieve limit from tooling
_twitterAuthenticationInitializer.EnsureAuthenticationIsInitialized();
ExceptionHandler.SwallowWebExceptions = false;
RateLimit.RateLimitTrackerMode = RateLimitTrackerMode.TrackOnly;
try
{
var queryRateLimits = RateLimit.GetQueryRateLimit("https://api.twitter.com/1.1/users/show.json?screen_name=mastodon");
if (queryRateLimits != null)
{
return queryRateLimits.Remaining <= 0;
}
}
catch (Exception e)
{
_logger.LogError(e, "Error retrieving rate limits");
}
// Fallback
var currentCalls = _statisticsHandler.GetCurrentUserCalls();
var maxCalls = _statisticsHandler.GetStatistics().UserCallsMax;
return currentCalls >= maxCalls;
}
}
}

View File

@ -1,18 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<UserSecretsId>d21486de-a812-47eb-a419-05682bb68856</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<Version>0.19.1</Version>
<Version>0.22.0</Version>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Lamar.Microsoft.DependencyInjection" Version="5.0.0" />
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.16.0" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.10.8" />
<PackageReference Include="Lamar.Microsoft.DependencyInjection" Version="12.0.0" />
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.21.0" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.18.1" />
<PackageReference Include="Microsoft.VisualStudio.Web.BrowserLink" Version="2.2.0" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.3" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.5" />
</ItemGroup>
<ItemGroup>

View File

@ -14,6 +14,7 @@ using Newtonsoft.Json;
namespace BirdsiteLive.Controllers
{
#if DEBUG
public class DebugingController : Controller
{
private readonly InstanceSettings _instanceSettings;
@ -58,17 +59,22 @@ namespace BirdsiteLive.Controllers
[HttpPost]
public async Task<IActionResult> PostNote()
{
var username = "gra";
var username = "twitter";
var actor = $"https://{_instanceSettings.Domain}/users/{username}";
var targetHost = "mastodon.technology";
var target = $"{targetHost}/users/testtest";
var inbox = $"/users/testtest/inbox";
var targetHost = "ioc.exchange";
var target = $"https://{targetHost}/users/test";
//var inbox = $"/users/testtest/inbox";
var inbox = $"/inbox";
var noteGuid = Guid.NewGuid();
var noteId = $"https://{_instanceSettings.Domain}/users/{username}/statuses/{noteGuid}";
var noteUrl = $"https://{_instanceSettings.Domain}/@{username}/{noteGuid}";
var to = $"{actor}/followers";
to = target;
var cc = new[] { "https://www.w3.org/ns/activitystreams#Public" };
cc = new string[0];
var now = DateTime.UtcNow;
var nowString = now.ToString("s") + "Z";
@ -80,12 +86,12 @@ namespace BirdsiteLive.Controllers
type = "Create",
actor = actor,
published = nowString,
to = new []{ to },
//cc = new [] { "https://www.w3.org/ns/activitystreams#Public" },
to = new[] { to },
cc = cc,
apObject = new Note()
{
id = noteId,
summary = null,
summary = null,
inReplyTo = null,
published = nowString,
url = noteUrl,
@ -93,7 +99,8 @@ namespace BirdsiteLive.Controllers
// Unlisted
to = new[] { to },
cc = new [] { "https://www.w3.org/ns/activitystreams#Public" },
cc = cc,
//cc = new[] { "https://www.w3.org/ns/activitystreams#Public" },
//// Public
//to = new[] { "https://www.w3.org/ns/activitystreams#Public" },
@ -101,8 +108,16 @@ namespace BirdsiteLive.Controllers
sensitive = false,
content = "<p>TEST PUBLIC</p>",
//content = "<p><span class=\"h-card\"><a href=\"https://ioc.exchange/users/test\" class=\"u-url mention\">@<span>test</span></a></span> test</p>",
attachment = new Attachment[0],
tag = new Tag[0]
tag = new Tag[]{
new Tag()
{
type = "Mention",
href = target,
name = "@test@ioc.exchange"
}
},
}
};
@ -124,7 +139,19 @@ namespace BirdsiteLive.Controllers
await _userService.SendRejectFollowAsync(activityFollow, "mastodon.technology");
return View("Index");
}
[HttpPost]
public async Task<IActionResult> PostDeleteUser()
{
var userName = "twitter";
var host = "ioc.exchange";
var inbox = "/inbox";
await _activityPubService.DeleteUserAsync(userName, host, inbox);
return View("Index");
}
}
#endif
public static class HtmlHelperExtensions
{

View File

@ -3,6 +3,10 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BirdsiteLive.ActivityPub;
using BirdsiteLive.ActivityPub.Models;
using BirdsiteLive.Domain;
using BirdsiteLive.Tools;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
@ -13,11 +17,13 @@ namespace BirdsiteLive.Controllers
public class InboxController : ControllerBase
{
private readonly ILogger<InboxController> _logger;
private readonly IUserService _userService;
#region Ctor
public InboxController(ILogger<InboxController> logger)
public InboxController(ILogger<InboxController> logger, IUserService userService)
{
_logger = logger;
_userService = userService;
}
#endregion
@ -25,15 +31,32 @@ namespace BirdsiteLive.Controllers
[HttpPost]
public async Task<IActionResult> Inbox()
{
var r = Request;
using (var reader = new StreamReader(Request.Body))
try
{
var body = await reader.ReadToEndAsync();
var r = Request;
using (var reader = new StreamReader(Request.Body))
{
var body = await reader.ReadToEndAsync();
_logger.LogTrace("Inbox: {Body}", body);
//System.IO.File.WriteAllText($@"C:\apdebug\inbox\{Guid.NewGuid()}.json", body);
_logger.LogTrace("Inbox: {Body}", body);
//System.IO.File.WriteAllText($@"C:\apdebug\inbox\{Guid.NewGuid()}.json", body);
var activity = ApDeserializer.ProcessActivity(body);
var signature = r.Headers["Signature"].First();
switch (activity?.type)
{
case "Delete":
{
var succeeded = await _userService.DeleteRequestedAsync(signature, r.Method, r.Path,
r.QueryString.ToString(), HeaderHandler.RequestHeaders(r.Headers), activity as ActivityDelete, body);
if (succeeded) return Accepted();
else return Unauthorized();
}
}
}
}
catch (FollowerIsGoneException) { } //TODO: check if user in DB
return Accepted();
}

View File

@ -0,0 +1,227 @@
using System;
using Microsoft.AspNetCore.Mvc;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Npgsql.TypeHandlers;
using BirdsiteLive.Domain;
using BirdsiteLive.Domain.Enum;
using BirdsiteLive.DAL.Contracts;
using BirdsiteLive.Models;
using System.Reflection.Metadata;
namespace BirdsiteLive.Controllers
{
public class MigrationController : Controller
{
private readonly MigrationService _migrationService;
private readonly ITwitterUserDal _twitterUserDal;
#region Ctor
public MigrationController(MigrationService migrationService, ITwitterUserDal twitterUserDal)
{
_migrationService = migrationService;
_twitterUserDal = twitterUserDal;
}
#endregion
[HttpGet]
[Route("/migration/move/{id}")]
public IActionResult IndexMove(string id)
{
var migrationCode = _migrationService.GetMigrationCode(id);
var data = new MigrationData()
{
Acct = id,
MigrationCode = migrationCode
};
return View("Index", data);
}
[HttpGet]
[Route("/migration/delete/{id}")]
public IActionResult IndexDelete(string id)
{
var migrationCode = _migrationService.GetDeletionCode(id);
var data = new MigrationData()
{
Acct = id,
MigrationCode = migrationCode
};
return View("Delete", data);
}
[HttpPost]
[Route("/migration/move/{id}")]
public async Task<IActionResult> MigrateMove(string id, string tweetid, string handle)
{
var migrationCode = _migrationService.GetMigrationCode(id);
var data = new MigrationData()
{
Acct = id,
MigrationCode = migrationCode,
IsAcctProvided = !string.IsNullOrWhiteSpace(handle),
IsTweetProvided = !string.IsNullOrWhiteSpace(tweetid),
TweetId = tweetid,
FediverseAccount = handle
};
ValidatedFediverseUser fediverseUserValidation = null;
//Verify can be migrated
var twitterAccount = await _twitterUserDal.GetTwitterUserAsync(id);
if (twitterAccount != null && twitterAccount.Deleted)
{
data.ErrorMessage = "This account has been deleted, it can't be migrated";
return View("Index", data);
}
if (twitterAccount != null &&
(!string.IsNullOrWhiteSpace(twitterAccount.MovedTo)
|| !string.IsNullOrWhiteSpace(twitterAccount.MovedToAcct)))
{
data.ErrorMessage = "This account has been moved already, it can't be migrated again";
return View("Index", data);
}
// Start migration
try
{
fediverseUserValidation = await _migrationService.ValidateFediverseAcctAsync(handle);
var isTweetValid = _migrationService.ValidateTweet(id, tweetid, MigrationTypeEnum.Migration);
data.IsAcctValid = fediverseUserValidation.IsValid;
data.IsTweetValid = isTweetValid;
}
catch (Exception e)
{
data.ErrorMessage = e.Message;
}
if (data.IsAcctValid && data.IsTweetValid && fediverseUserValidation != null)
{
try
{
await _migrationService.MigrateAccountAsync(fediverseUserValidation, id);
_migrationService.TriggerRemoteMigrationAsync(id, tweetid, handle);
data.MigrationSuccess = true;
}
catch (Exception e)
{
Console.WriteLine(e);
data.ErrorMessage = e.Message;
}
}
return View("Index", data);
}
[HttpPost]
[Route("/migration/delete/{id}")]
public async Task<IActionResult> MigrateDelete(string id, string tweetid)
{
var deletionCode = _migrationService.GetDeletionCode(id);
var data = new MigrationData()
{
Acct = id,
MigrationCode = deletionCode,
IsTweetProvided = !string.IsNullOrWhiteSpace(tweetid),
TweetId = tweetid
};
//Verify can be deleted
var twitterAccount = await _twitterUserDal.GetTwitterUserAsync(id);
if (twitterAccount != null && twitterAccount.Deleted)
{
data.ErrorMessage = "This account has been deleted, it can't be deleted again";
return View("Delete", data);
}
// Start deletion
try
{
var isTweetValid = _migrationService.ValidateTweet(id, tweetid, MigrationTypeEnum.Deletion);
data.IsTweetValid = isTweetValid;
}
catch (Exception e)
{
data.ErrorMessage = e.Message;
}
if (data.IsTweetValid)
{
try
{
await _migrationService.DeleteAccountAsync(id);
_migrationService.TriggerRemoteDeleteAsync(id, tweetid);
data.MigrationSuccess = true;
}
catch (Exception e)
{
Console.WriteLine(e);
data.ErrorMessage = e.Message;
}
}
return View("Delete", data);
}
[HttpPost]
[Route("/migration/move/{id}/{tweetid}/{handle}")]
public async Task<IActionResult> RemoteMigrateMove(string id, string tweetid, string handle)
{
//Check inputs
if (string.IsNullOrWhiteSpace(id) || string.IsNullOrWhiteSpace(tweetid) ||
string.IsNullOrWhiteSpace(handle))
return StatusCode(422);
//Verify can be migrated
var twitterAccount = await _twitterUserDal.GetTwitterUserAsync(id);
if (twitterAccount != null && (twitterAccount.Deleted
|| !string.IsNullOrWhiteSpace(twitterAccount.MovedTo)
|| !string.IsNullOrWhiteSpace(twitterAccount.MovedToAcct)))
return Ok();
// Start migration
var fediverseUserValidation = await _migrationService.ValidateFediverseAcctAsync(handle);
var isTweetValid = _migrationService.ValidateTweet(id, tweetid, MigrationTypeEnum.Migration);
if (fediverseUserValidation.IsValid && isTweetValid)
{
await _migrationService.MigrateAccountAsync(fediverseUserValidation, id);
return Ok();
}
return StatusCode(400);
}
[HttpPost]
[Route("/migration/delete/{id}/{tweetid}")]
public async Task<IActionResult> RemoteMigrateDelete(string id, string tweetid)
{
//Check inputs
if (string.IsNullOrWhiteSpace(id) || string.IsNullOrWhiteSpace(tweetid))
return StatusCode(422);
//Verify can be deleted
var twitterAccount = await _twitterUserDal.GetTwitterUserAsync(id);
if (twitterAccount != null && twitterAccount.Deleted) return Ok();
// Start deletion
var isTweetValid = _migrationService.ValidateTweet(id, tweetid, MigrationTypeEnum.Deletion);
if (isTweetValid)
{
await _migrationService.DeleteAccountAsync(id);
return Ok();
}
return StatusCode(400);
}
}
}

View File

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Mime;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
@ -11,8 +10,11 @@ using BirdsiteLive.ActivityPub;
using BirdsiteLive.ActivityPub.Models;
using BirdsiteLive.Common.Regexes;
using BirdsiteLive.Common.Settings;
using BirdsiteLive.DAL.Contracts;
using BirdsiteLive.DAL.Models;
using BirdsiteLive.Domain;
using BirdsiteLive.Models;
using BirdsiteLive.Tools;
using BirdsiteLive.Twitter;
using BirdsiteLive.Twitter.Models;
using Microsoft.AspNetCore.Http;
@ -27,6 +29,7 @@ namespace BirdsiteLive.Controllers
{
private readonly ITwitterUserService _twitterUserService;
private readonly ITwitterTweetsService _twitterTweetService;
private readonly ITwitterUserDal _twitterUserDal;
private readonly IUserService _userService;
private readonly IStatusService _statusService;
private readonly InstanceSettings _instanceSettings;
@ -34,7 +37,7 @@ namespace BirdsiteLive.Controllers
private readonly ILogger<UsersController> _logger;
#region Ctor
public UsersController(ITwitterUserService twitterUserService, IUserService userService, IStatusService statusService, InstanceSettings instanceSettings, ITwitterTweetsService twitterTweetService, IActivityPubService activityPubService, ILogger<UsersController> logger)
public UsersController(ITwitterUserService twitterUserService, IUserService userService, IStatusService statusService, InstanceSettings instanceSettings, ITwitterTweetsService twitterTweetService, IActivityPubService activityPubService, ILogger<UsersController> logger, ITwitterUserDal twitterUserDal)
{
_twitterUserService = twitterUserService;
_userService = userService;
@ -43,6 +46,7 @@ namespace BirdsiteLive.Controllers
_twitterTweetService = twitterTweetService;
_activityPubService = activityPubService;
_logger = logger;
_twitterUserDal = twitterUserDal;
}
#endregion
@ -57,21 +61,52 @@ namespace BirdsiteLive.Controllers
}
return View("UserNotFound");
}
[Route("/@{id}")]
[Route("/users/{id}")]
[Route("/users/{id}/remote_follow")]
public IActionResult Index(string id)
public async Task<IActionResult> Index(string id)
{
_logger.LogTrace("User Index: {Id}", id);
id = id.Trim(new[] { ' ', '@' }).ToLowerInvariant();
TwitterUser user = null;
var isSaturated = false;
var notFound = false;
// Ensure valid username
// https://help.twitter.com/en/managing-your-account/twitter-username-rules
TwitterUser user = null;
if (!string.IsNullOrWhiteSpace(id) && UserRegexes.TwitterAccount.IsMatch(id) && id.Length <= 15)
user = _twitterUserService.GetUser(id);
{
try
{
user = _twitterUserService.GetUser(id);
}
catch (UserNotFoundException)
{
notFound = true;
}
catch (UserHasBeenSuspendedException)
{
notFound = true;
}
catch (RateLimitExceededException)
{
isSaturated = true;
}
catch (Exception e)
{
_logger.LogError(e, "Exception getting {Id}", id);
throw;
}
}
else
{
notFound = true;
}
//var isSaturated = _twitterUserService.IsUserApiRateLimited();
var dbUser = await _twitterUserDal.GetTwitterUserAsync(id);
var acceptHeaders = Request.Headers["Accept"];
if (acceptHeaders.Any())
@ -79,8 +114,10 @@ namespace BirdsiteLive.Controllers
var r = acceptHeaders.First();
if (r.Contains("application/activity+json"))
{
if (user == null) return NotFound();
var apUser = _userService.GetUser(user);
if (isSaturated) return new ObjectResult("Too Many Requests") { StatusCode = 429 };
if (notFound) return NotFound();
if (dbUser != null && dbUser.Deleted) return new ObjectResult("Gone") { StatusCode = 410 };
var apUser = _userService.GetUser(user, dbUser);
var jsonApUser = JsonConvert.SerializeObject(apUser, new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore
@ -89,7 +126,8 @@ namespace BirdsiteLive.Controllers
}
}
if (user == null) return View("UserNotFound");
if (isSaturated) return View("ApiSaturated");
if (notFound) return View("UserNotFound");
var displayableUser = new DisplayTwitterUser
{
@ -99,11 +137,21 @@ namespace BirdsiteLive.Controllers
Url = user.Url,
ProfileImageUrl = user.ProfileImageUrl,
Protected = user.Protected,
InstanceHandle = $"@{user.Acct.ToLowerInvariant()}@{_instanceSettings.Domain}",
InstanceHandle = $"@{user.Acct.ToLowerInvariant()}@{_instanceSettings.Domain}"
MovedTo = dbUser?.MovedTo,
MovedToAcct = dbUser?.MovedToAcct,
Deleted = dbUser?.Deleted ?? false,
};
return View(displayableUser);
}
[Route("/users/{id}/remote_follow")]
public async Task<IActionResult> IndexRemoteFollow(string id)
{
return Redirect($"/users/{id}");
}
[Route("/@{id}/{statusId}")]
[Route("/users/{id}/statuses/{statusId}")]
@ -150,41 +198,69 @@ namespace BirdsiteLive.Controllers
[HttpPost]
public async Task<IActionResult> Inbox()
{
var r = Request;
using (var reader = new StreamReader(Request.Body))
try
{
var body = await reader.ReadToEndAsync();
_logger.LogTrace("User Inbox: {Body}", body);
//System.IO.File.WriteAllText($@"C:\apdebug\{Guid.NewGuid()}.json", body);
var activity = ApDeserializer.ProcessActivity(body);
// Do something
var signature = r.Headers["Signature"].First();
switch (activity?.type)
var r = Request;
using (var reader = new StreamReader(Request.Body))
{
case "Follow":
var body = await reader.ReadToEndAsync();
_logger.LogTrace("User Inbox: {Body}", body);
//System.IO.File.WriteAllText($@"C:\apdebug\{Guid.NewGuid()}.json", body);
var activity = ApDeserializer.ProcessActivity(body);
var signature = r.Headers["Signature"].First();
switch (activity?.type)
{
case "Follow":
{
var succeeded = await _userService.FollowRequestedAsync(signature, r.Method, r.Path,
r.QueryString.ToString(), RequestHeaders(r.Headers), activity as ActivityFollow, body);
r.QueryString.ToString(), HeaderHandler.RequestHeaders(r.Headers),
activity as ActivityFollow, body);
if (succeeded) return Accepted();
else return Unauthorized();
}
case "Undo":
if (activity is ActivityUndoFollow)
{
var succeeded = await _userService.UndoFollowRequestedAsync(signature, r.Method, r.Path,
r.QueryString.ToString(), HeaderHandler.RequestHeaders(r.Headers),
activity as ActivityUndoFollow, body);
if (succeeded) return Accepted();
else return Unauthorized();
}
if (succeeded) return Accepted();
else return Unauthorized();
}
case "Undo":
if (activity is ActivityUndoFollow)
return Accepted();
case "Delete":
{
var succeeded = await _userService.UndoFollowRequestedAsync(signature, r.Method, r.Path,
r.QueryString.ToString(), RequestHeaders(r.Headers), activity as ActivityUndoFollow, body);
var succeeded = await _userService.DeleteRequestedAsync(signature, r.Method, r.Path,
r.QueryString.ToString(), HeaderHandler.RequestHeaders(r.Headers),
activity as ActivityDelete, body);
if (succeeded) return Accepted();
else return Unauthorized();
}
return Accepted();
default:
return Accepted();
default:
return Accepted();
}
}
}
catch (FollowerIsGoneException) //TODO: check if user in DB
{
return Accepted();
}
catch (UserNotFoundException)
{
return NotFound();
}
catch (UserHasBeenSuspendedException)
{
return NotFound();
}
catch (RateLimitExceededException)
{
return new ObjectResult("Too Many Requests") { StatusCode = 429 };
}
}
[Route("/users/{id}/followers")]
@ -202,11 +278,6 @@ namespace BirdsiteLive.Controllers
return Content(jsonApUser, "application/activity+json; charset=utf-8");
}
private Dictionary<string, string> RequestHeaders(IHeaderDictionary header)
{
return header.ToDictionary<KeyValuePair<string, StringValues>, string, string>(h => h.Key.ToLowerInvariant(), h => h.Value);
}
[Route("/users/{actor}/remote_follow")]
[HttpPost]
public async Task<IActionResult> RemoteFollow(string actor)

View File

@ -12,6 +12,7 @@ using BirdsiteLive.Models;
using BirdsiteLive.Models.WellKnownModels;
using BirdsiteLive.Twitter;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace BirdsiteLive.Controllers
@ -23,13 +24,15 @@ namespace BirdsiteLive.Controllers
private readonly ITwitterUserService _twitterUserService;
private readonly ITwitterUserDal _twitterUserDal;
private readonly InstanceSettings _settings;
private readonly ILogger<WellKnownController> _logger;
#region Ctor
public WellKnownController(InstanceSettings settings, ITwitterUserService twitterUserService, ITwitterUserDal twitterUserDal, IModerationRepository moderationRepository)
public WellKnownController(InstanceSettings settings, ITwitterUserService twitterUserService, ITwitterUserDal twitterUserDal, IModerationRepository moderationRepository, ILogger<WellKnownController> logger)
{
_twitterUserService = twitterUserService;
_twitterUserDal = twitterUserDal;
_moderationRepository = moderationRepository;
_logger = logger;
_settings = settings;
}
#endregion
@ -140,30 +143,54 @@ namespace BirdsiteLive.Controllers
[Route("/.well-known/webfinger")]
public IActionResult Webfinger(string resource = null)
{
var acct = resource.Split("acct:")[1].Trim();
if (string.IsNullOrWhiteSpace(resource))
return BadRequest();
string name = null;
string domain = null;
var splitAcct = acct.Split('@', StringSplitOptions.RemoveEmptyEntries);
if (resource.StartsWith("acct:"))
{
var acct = resource.Split("acct:")[1].Trim();
var splitAcct = acct.Split('@', StringSplitOptions.RemoveEmptyEntries);
var atCount = acct.Count(x => x == '@');
if (atCount == 1 && acct.StartsWith('@'))
{
name = splitAcct[1];
var atCount = acct.Count(x => x == '@');
if (atCount == 1 && acct.StartsWith('@'))
{
name = splitAcct[1];
}
else if (atCount == 1 || atCount == 2)
{
name = splitAcct[0];
domain = splitAcct[1];
}
else
{
return BadRequest();
}
}
else if (atCount == 1 || atCount == 2)
else if (resource.StartsWith("https://"))
{
name = splitAcct[0];
domain = splitAcct[1];
try
{
name = resource.Split('/').Last().Trim();
domain = resource.Split("https://", StringSplitOptions.RemoveEmptyEntries)[0].Split('/')[0].Trim();
}
catch (Exception e)
{
_logger.LogError(e, "Error parsing {Resource}", resource);
throw new NotImplementedException();
}
}
else
{
return BadRequest();
_logger.LogError("Error parsing {Resource}", resource);
throw new NotImplementedException();
}
// Ensure lowercase
name = name.ToLowerInvariant();
domain = domain?.ToLowerInvariant();
// Ensure valid username
// https://help.twitter.com/en/managing-your-account/twitter-username-rules
@ -173,9 +200,27 @@ namespace BirdsiteLive.Controllers
if (!string.IsNullOrWhiteSpace(domain) && domain != _settings.Domain)
return NotFound();
var user = _twitterUserService.GetUser(name);
if (user == null)
try
{
_twitterUserService.GetUser(name);
}
catch (UserNotFoundException)
{
return NotFound();
}
catch (UserHasBeenSuspendedException)
{
return NotFound();
}
catch (RateLimitExceededException)
{
return new ObjectResult("Too Many Requests") { StatusCode = 429 };
}
catch (Exception e)
{
_logger.LogError(e, "Exception getting {Name}", name);
throw;
}
var actorUrl = UrlFactory.GetActorUrl(_settings.Domain, name);

View File

@ -10,5 +10,9 @@
public bool Protected { get; set; }
public string InstanceHandle { get; set; }
public string MovedTo { get; set; }
public string MovedToAcct { get; set; }
public bool Deleted { get; set; }
}
}

View File

@ -0,0 +1,21 @@
namespace BirdsiteLive.Models
{
public class MigrationData
{
public string Acct { get; set; }
public string FediverseAccount { get; set; }
public string TweetId { get; set; }
public string MigrationCode { get; set; }
public bool IsTweetProvided { get; set; }
public bool IsAcctProvided { get; set; }
public bool IsTweetValid { get; set; }
public bool IsAcctValid { get; set; }
public string ErrorMessage { get; set; }
public bool MigrationSuccess { get; set; }
}
}

View File

@ -14,7 +14,7 @@ namespace BirdsiteLive
{
public class Program
{
public static string VERSION = System.Reflection.Assembly.GetEntryAssembly().GetName().Version.ToString(3) + "+pasture";
public static string VERSION = System.Reflection.Assembly.GetEntryAssembly().GetName().Version.ToString(3) + "-fishe";
public static void Main(string[] args)
{

View File

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using BirdsiteLive.Common.Settings;
using BirdsiteLive.Common.Structs;
@ -50,7 +51,12 @@ namespace BirdsiteLive
services.AddControllersWithViews();
services.AddHttpClient();
services.AddHttpClient("BirdsiteLIVE", httpClient => {
ProductInfoHeaderValue product = new("BirdsiteLIVE", $"{Program.VERSION}");
ProductInfoHeaderValue comment = new($"(+https://{Configuration["Instance:Domain"]})");
httpClient.DefaultRequestHeaders.UserAgent.Add(product);
httpClient.DefaultRequestHeaders.UserAgent.Add(comment);
});
}
public void ConfigureContainer(ServiceRegistry services)

View File

@ -0,0 +1,15 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
namespace BirdsiteLive.Tools
{
public class HeaderHandler
{
public static Dictionary<string, string> RequestHeaders(IHeaderDictionary header)
{
return header.ToDictionary<KeyValuePair<string, StringValues>, string, string>(h => h.Key.ToLowerInvariant(), h => h.Value);
}
}
}

View File

@ -56,7 +56,7 @@
As this instance's saturation level approaches and exceeds 100%, it will no longer update accounts every 15 minutes and reduce how often it fetches Tweets to stay under Twitter's limits. Essentially, the more saturated a node is, the less efficient it will be.<br /><br />
When possible, you should <a href="https://git.gamers.exposed/pasture/BirdsiteLIVE/src/branch/master/INSTALLATION.md" target="_blank">start your own BirdsiteLIVE instance</a>. If you cannot, please be courteous and follow a limited number of accounts to keep the service available for everyone.
When possible, you should <a href="https://git.froth.zone/sam/BirdsiteLIVE/src/branch/master/INSTALLATION.md" target="_blank">start your own BirdsiteLIVE instance</a>. If you cannot, please be courteous and follow a limited number of accounts to keep the service available for everyone.
</p>
@if (Model.Settings.DiscloseInstanceRestrictions && (Model.ModerationStatus.Followers != BirdsiteLive.Domain.Repository.ModerationTypeEnum.None || Model.ModerationStatus.TwitterAccounts != BirdsiteLive.Domain.Repository.ModerationTypeEnum.None))

View File

@ -23,4 +23,10 @@
<!-- Input and Submit elements -->
<button type="submit" value="Submit">Reject Follow</button>
</form>
<form asp-controller="Debuging" asp-action="PostDeleteUser" method="post">
<!-- Input and Submit elements -->
<button type="submit" value="Submit">Delete User</button>
</form>

View File

@ -0,0 +1,51 @@
@model MigrationData
@{
ViewData["Title"] = "Migration";
}
<div class="col-12 col-sm-10 col-md-8 col-lg-6 mx-auto">
@if (!string.IsNullOrWhiteSpace(ViewData.Model.ErrorMessage))
{
<div class="alert alert-danger" role="alert">
@ViewData.Model.ErrorMessage
</div>
}
@if (ViewData.Model.MigrationSuccess)
{
<div class="alert alert-success" role="alert">
The mirror has been successfully deleted
</div>
}
<h1 class="display-4 migration__title">Delete @@@ViewData.Model.Acct mirror</h1>
@if (!ViewData.Model.IsTweetProvided)
{
<h2 class="display-4 migration__subtitle">What is needed?</h2>
<p>You'll need access to the Twitter account to provide proof of ownership.</p>
<h2 class="display-4 migration__subtitle">What will deletion do?</h2>
<p>
Deletion will remove all followers, delete the account and will be blacklisted so that it can't be recreated.<br />
</p>
}
<h2 class="display-4 migration__subtitle">Start the deletion!</h2>
<p>Please copy and post this string in a public Tweet (the string must be untampered, but you can write anything you want before or after it):</p>
<input type="text" name="textbox" value="@ViewData.Model.MigrationCode" onclick="this.select()" class="form-control" readonly />
<br />
<h2 class="display-4 migration__subtitle">Provide deletion information:</h2>
<form method="POST">
<div class="form-group">
<label for="tweetid">Tweet URL</label>
<input type="text" class="form-control" id="tweetid" name="tweetid" autocomplete="off" placeholder="https://twitter.com/<username>/status/<tweet id>" value="@ViewData.Model.TweetId">
</div>
<button type="submit" class="btn btn-primary">Delete!</button>
</form>
</div>

View File

@ -0,0 +1,66 @@
@model MigrationData
@{
ViewData["Title"] = "Migration";
}
<div class="col-12 col-sm-10 col-md-8 col-lg-6 mx-auto">
@if (!string.IsNullOrWhiteSpace(ViewData.Model.ErrorMessage))
{
<div class="alert alert-danger" role="alert">
@ViewData.Model.ErrorMessage
</div>
}
@if (ViewData.Model.MigrationSuccess)
{
<div class="alert alert-success" role="alert">
The mirror has been successfully migrated
</div>
}
<h1 class="display-4 migration__title">Migrate @@@ViewData.Model.Acct mirror to my Fediverse account</h1>
@if (!ViewData.Model.IsAcctProvided && !ViewData.Model.IsTweetProvided)
{
<h2 class="display-4 migration__subtitle">What is needed?</h2>
<p>You'll need a Fediverse account and access to the Twitter account to provide proof of ownership.</p>
<h2 class="display-4 migration__subtitle">What will migration do?</h2>
<p>
Migration will notify followers of the migration of the mirror account to your fediverse account and will be disabled after that.<br />
</p>
}
<h2 class="display-4 migration__subtitle">Start the migration!</h2>
<p>Please copy and post this string in a public Tweet (the string must be untampered, but you can write anything you want before or after it):</p>
<input type="text" name="textbox" value="@ViewData.Model.MigrationCode" onclick="this.select()" class="form-control" readonly />
<br />
<h2 class="display-4 migration__subtitle">Provide migration information:</h2>
<form method="POST">
@*<div class="form-group">
<label for="exampleInputEmail1">Email address</label>
<input type="email" class="form-control" id="exampleInputEmail1" aria-describedby="emailHelp" placeholder="Enter email">
<small id="emailHelp" class="form-text text-muted">We'll never share your email with anyone else.</small>
</div>*@
<div class="form-group">
<label for="handle">Fediverse target account</label>
<input type="text" class="form-control" id="handle" name="handle" autocomplete="off" placeholder="@Html.Raw("@username@domain.ext")" value="@ViewData.Model.FediverseAccount">
</div>
<div class="form-group">
<label for="tweetid">Tweet URL</label>
<input type="text" class="form-control" id="tweetid" name="tweetid" autocomplete="off" placeholder="https://twitter.com/<username>/status/<tweet id>" value="@ViewData.Model.TweetId">
</div>
<button type="submit" class="btn btn-primary">Migrate!</button>
</form>
<br />
<br />
<br />
<div class="user-owner">
<a href="/migration/delete/@ViewData.Model.Acct">I don't have a fediverse account and I'd like to delete this mirror.</a>
</div>
</div>

View File

@ -47,7 +47,7 @@
</div>
<div class="container">
<a href="https://git.gamers.exposed/pasture/BirdsiteLIVE">Source code</a> (AGPLv3) @*<a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>*@
<a href="https://git.froth.zone/sam/BirdsiteLIVE">Source code</a> (AGPLv3) @*<a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>*@
<span style="float: right;">BirdsiteLIVE @Program.VERSION</span>
</div>

View File

@ -0,0 +1,13 @@
@using BirdsiteLive.Controllers;
@{
ViewData["Title"] = "Api Saturated";
}
<div class="text-center">
<h1 class="display-4">429 Too Many Requests</h1>
<p>
<br />
The API is saturated.<br/>
Please consider using another instance.
</p>
</div>

View File

@ -31,7 +31,20 @@
<br />
<br />
@if (ViewData.Model.Protected)
@if (ViewData.Model.Deleted)
{
<div class="alert alert-danger" role="alert">
This mirror has been deleted by its Twitter owner.
</div>
}
else if (!string.IsNullOrEmpty(ViewData.Model.MovedTo))
{
<div class="alert alert-danger" role="alert">
This account has been migrated by its Twitter owner and has been disabled.<br />
You can follow this user at <a href="@ViewData.Model.MovedTo">@ViewData.Model.MovedToAcct</a>.
</div>
}
else if (ViewData.Model.Protected)
{
<div class="alert alert-danger" role="alert">
This account is protected, BirdsiteLIVE cannot fetch their tweets and will not provide follow support until it is unprotected again.
@ -50,4 +63,8 @@
<input type="text" name="textbox" value="@ViewData.Model.InstanceHandle" onclick="this.select()" class="form-control" readonly />
</div>
}
<div class="user-owner">
<a href="/migration/move/@ViewData.Model.Acct">I'm the owner of this account and I would like to take control of this mirror.</a>
</div>
</div>

View File

@ -30,7 +30,9 @@
"DiscloseInstanceRestrictions": false,
"SensitiveTwitterAccounts": null,
"FailingTwitterUserCleanUpThreshold": 700,
"MaxStatusFetchAge": 0
"MaxStatusFetchAge": 0,
"FailingFollowerCleanUpThreshold": 30000,
"UserCacheCapacity": 10000
},
"Db": {
"Type": "postgres",

View File

@ -71,3 +71,18 @@
margin-left: 60px;
/*font-weight: bold;*/
}
.user-owner {
font-size: .8em;
padding-top: 20px;
}
/** Migration **/
.migration__title {
font-size: 1.8em;
}
.migration__subtitle {
font-size: 1.4em;
}

View File

@ -1,12 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="2.0.35" />
<PackageReference Include="Npgsql" Version="4.1.3.1" />
<PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="Npgsql" Version="4.1.12" />
</ItemGroup>
<ItemGroup>

View File

@ -23,7 +23,7 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
public class DbInitializerPostgresDal : PostgresBase, IDbInitializerDal
{
private readonly PostgresTools _tools;
private readonly Version _currentVersion = new Version(2, 3);
private readonly Version _currentVersion = new Version(2, 5);
private const string DbVersionType = "db-version";
#region Ctor
@ -134,7 +134,9 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
new Tuple<Version, Version>(new Version(1,0), new Version(2,0)),
new Tuple<Version, Version>(new Version(2,0), new Version(2,1)),
new Tuple<Version, Version>(new Version(2,1), new Version(2,2)),
new Tuple<Version, Version>(new Version(2,2), new Version(2,3))
new Tuple<Version, Version>(new Version(2,2), new Version(2,3)),
new Tuple<Version, Version>(new Version(2,3), new Version(2,4)),
new Tuple<Version, Version>(new Version(2,4), new Version(2,5))
};
}
@ -163,6 +165,25 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
var addPostingError = $@"ALTER TABLE {_settings.FollowersTableName} ADD postingErrorCount SMALLINT";
await _tools.ExecuteRequestAsync(addPostingError);
}
else if (from == new Version(2, 3) && to == new Version(2, 4))
{
var alterLastSync = $@"ALTER TABLE {_settings.TwitterUserTableName} ALTER COLUMN fetchingErrorCount TYPE INTEGER";
await _tools.ExecuteRequestAsync(alterLastSync);
var alterPostingError = $@"ALTER TABLE {_settings.FollowersTableName} ALTER COLUMN postingErrorCount TYPE INTEGER";
await _tools.ExecuteRequestAsync(alterPostingError);
}
else if (from == new Version(2, 4) && to == new Version(2, 5))
{
var addMovedTo = $@"ALTER TABLE {_settings.TwitterUserTableName} ADD movedTo VARCHAR(2048)";
await _tools.ExecuteRequestAsync(addMovedTo);
var addMovedToAcct = $@"ALTER TABLE {_settings.TwitterUserTableName} ADD movedToAcct VARCHAR(305)";
await _tools.ExecuteRequestAsync(addMovedToAcct);
var addDeletedToAcct = $@"ALTER TABLE {_settings.TwitterUserTableName} ADD deleted BOOLEAN";
await _tools.ExecuteRequestAsync(addDeletedToAcct);
}
else
{
throw new NotImplementedException();

View File

@ -18,7 +18,7 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
}
#endregion
public async Task CreateTwitterUserAsync(string acct, long lastTweetPostedId)
public async Task CreateTwitterUserAsync(string acct, long lastTweetPostedId, string movedTo = null, string movedToAcct = null)
{
acct = acct.ToLowerInvariant();
@ -27,8 +27,15 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
dbConnection.Open();
await dbConnection.ExecuteAsync(
$"INSERT INTO {_settings.TwitterUserTableName} (acct,lastTweetPostedId,lastTweetSynchronizedForAllFollowersId) VALUES(@acct,@lastTweetPostedId,@lastTweetSynchronizedForAllFollowersId)",
new { acct, lastTweetPostedId, lastTweetSynchronizedForAllFollowersId = lastTweetPostedId });
$"INSERT INTO {_settings.TwitterUserTableName} (acct,lastTweetPostedId,lastTweetSynchronizedForAllFollowersId, movedTo, movedToAcct) VALUES(@acct,@lastTweetPostedId,@lastTweetSynchronizedForAllFollowersId,@movedTo,@movedToAcct)",
new
{
acct,
lastTweetPostedId,
lastTweetSynchronizedForAllFollowersId = lastTweetPostedId,
movedTo,
movedToAcct
});
}
}
@ -62,7 +69,7 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
public async Task<int> GetTwitterUsersCountAsync()
{
var query = $"SELECT COUNT(*) FROM {_settings.TwitterUserTableName}";
var query = $"SELECT COUNT(*) FROM {_settings.TwitterUserTableName} WHERE (movedTo = '') IS NOT FALSE AND deleted IS NOT TRUE";
using (var dbConnection = Connection)
{
@ -75,7 +82,7 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
public async Task<int> GetFailingTwitterUsersCountAsync()
{
var query = $"SELECT COUNT(*) FROM {_settings.TwitterUserTableName} WHERE fetchingErrorCount > 0";
var query = $"SELECT COUNT(*) FROM {_settings.TwitterUserTableName} WHERE fetchingErrorCount > 0 AND (movedTo = '') IS NOT FALSE AND deleted IS NOT TRUE";
using (var dbConnection = Connection)
{
@ -86,9 +93,10 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
}
}
public async Task<SyncTwitterUser[]> GetAllTwitterUsersAsync(int maxNumber)
public async Task<SyncTwitterUser[]> GetAllTwitterUsersAsync(int maxNumber, bool retrieveDisabledUser)
{
var query = $"SELECT * FROM {_settings.TwitterUserTableName} ORDER BY lastSync ASC NULLS FIRST LIMIT @maxNumber";
var query = $"SELECT * FROM {_settings.TwitterUserTableName} WHERE (movedTo = '') IS NOT FALSE AND deleted IS NOT TRUE ORDER BY lastSync ASC NULLS FIRST LIMIT @maxNumber";
if (retrieveDisabledUser) query = $"SELECT * FROM {_settings.TwitterUserTableName} ORDER BY lastSync ASC NULLS FIRST LIMIT @maxNumber";
using (var dbConnection = Connection)
{
@ -99,9 +107,10 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
}
}
public async Task<SyncTwitterUser[]> GetAllTwitterUsersAsync()
public async Task<SyncTwitterUser[]> GetAllTwitterUsersAsync(bool retrieveDisabledUser)
{
var query = $"SELECT * FROM {_settings.TwitterUserTableName}";
var query = $"SELECT * FROM {_settings.TwitterUserTableName} WHERE (movedTo = '') IS NOT FALSE AND deleted IS NOT TRUE";
if(retrieveDisabledUser) query = $"SELECT * FROM {_settings.TwitterUserTableName}";
using (var dbConnection = Connection)
{
@ -112,26 +121,36 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
}
}
public async Task UpdateTwitterUserAsync(int id, long lastTweetPostedId, long lastTweetSynchronizedForAllFollowersId, int fetchingErrorCount, DateTime lastSync)
public async Task UpdateTwitterUserAsync(int id, long lastTweetPostedId, long lastTweetSynchronizedForAllFollowersId, int fetchingErrorCount, DateTime lastSync, string movedTo, string movedToAcct, bool deleted)
{
if(id == default) throw new ArgumentException("id");
if(lastTweetPostedId == default) throw new ArgumentException("lastTweetPostedId");
if(lastTweetSynchronizedForAllFollowersId == default) throw new ArgumentException("lastTweetSynchronizedForAllFollowersId");
if(lastSync == default) throw new ArgumentException("lastSync");
var query = $"UPDATE {_settings.TwitterUserTableName} SET lastTweetPostedId = @lastTweetPostedId, lastTweetSynchronizedForAllFollowersId = @lastTweetSynchronizedForAllFollowersId, fetchingErrorCount = @fetchingErrorCount, lastSync = @lastSync WHERE id = @id";
var query = $"UPDATE {_settings.TwitterUserTableName} SET lastTweetPostedId = @lastTweetPostedId, lastTweetSynchronizedForAllFollowersId = @lastTweetSynchronizedForAllFollowersId, fetchingErrorCount = @fetchingErrorCount, lastSync = @lastSync, movedTo = @movedTo, movedToAcct = @movedToAcct, deleted = @deleted WHERE id = @id";
using (var dbConnection = Connection)
{
dbConnection.Open();
await dbConnection.QueryAsync(query, new { id, lastTweetPostedId, lastTweetSynchronizedForAllFollowersId, fetchingErrorCount, lastSync = lastSync.ToUniversalTime() });
await dbConnection.QueryAsync(query, new
{
id,
lastTweetPostedId,
lastTweetSynchronizedForAllFollowersId,
fetchingErrorCount,
lastSync = lastSync.ToUniversalTime(),
movedTo,
movedToAcct,
deleted
});
}
}
public async Task UpdateTwitterUserAsync(SyncTwitterUser user)
{
await UpdateTwitterUserAsync(user.Id, user.LastTweetPostedId, user.LastTweetSynchronizedForAllFollowersId, user.FetchingErrorCount, user.LastSync);
await UpdateTwitterUserAsync(user.Id, user.LastTweetPostedId, user.LastTweetSynchronizedForAllFollowersId, user.FetchingErrorCount, user.LastSync, user.MovedTo, user.MovedToAcct, user.Deleted);
}
public async Task DeleteTwitterUserAsync(string acct)

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>

View File

@ -6,12 +6,13 @@ namespace BirdsiteLive.DAL.Contracts
{
public interface ITwitterUserDal
{
Task CreateTwitterUserAsync(string acct, long lastTweetPostedId);
Task CreateTwitterUserAsync(string acct, long lastTweetPostedId, string movedTo = null,
string movedToAcct = null);
Task<SyncTwitterUser> GetTwitterUserAsync(string acct);
Task<SyncTwitterUser> GetTwitterUserAsync(int id);
Task<SyncTwitterUser[]> GetAllTwitterUsersAsync(int maxNumber);
Task<SyncTwitterUser[]> GetAllTwitterUsersAsync();
Task UpdateTwitterUserAsync(int id, long lastTweetPostedId, long lastTweetSynchronizedForAllFollowersId, int fetchingErrorCount, DateTime lastSync);
Task<SyncTwitterUser[]> GetAllTwitterUsersAsync(int maxNumber, bool retrieveDisabledUser);
Task<SyncTwitterUser[]> GetAllTwitterUsersAsync(bool retrieveDisabledUser);
Task UpdateTwitterUserAsync(int id, long lastTweetPostedId, long lastTweetSynchronizedForAllFollowersId, int fetchingErrorCount, DateTime lastSync, string movedTo, string movedToAcct, bool deleted);
Task UpdateTwitterUserAsync(SyncTwitterUser user);
Task DeleteTwitterUserAsync(string acct);
Task DeleteTwitterUserAsync(int id);

View File

@ -12,6 +12,11 @@ namespace BirdsiteLive.DAL.Models
public DateTime LastSync { get; set; }
public int FetchingErrorCount { get; set; } //TODO: update DAL
public int FetchingErrorCount { get; set; }
public string MovedTo { get; set; }
public string MovedToAcct { get; set; }
public bool Deleted { get; set; }
}
}

View File

@ -1,20 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="MSTest.TestAdapter" Version="2.1.1" />
<PackageReference Include="MSTest.TestFramework" Version="2.1.1" />
<PackageReference Include="coverlet.collector" Version="1.3.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.0.3" />
<PackageReference Include="MSTest.TestFramework" Version="3.0.3" />
<PackageReference Include="coverlet.collector" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\BSLManager\BSLManager.csproj" />
</ItemGroup>
</Project>
</Project>

View File

@ -1,4 +1,5 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using BirdsiteLive.ActivityPub.Models;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
namespace BirdsiteLive.ActivityPub.Tests
@ -48,6 +49,20 @@ namespace BirdsiteLive.ActivityPub.Tests
Assert.AreEqual("https://mamot.fr/users/testtest", data.apObject.apObject);
}
[TestMethod]
public void DeleteDeserializationTest()
{
var json =
"{\"@context\": \"https://www.w3.org/ns/activitystreams\", \"id\": \"https://mastodon.technology/users/deleteduser#delete\", \"type\": \"Delete\", \"actor\": \"https://mastodon.technology/users/deleteduser\", \"to\": [\"https://www.w3.org/ns/activitystreams#Public\"],\"object\": \"https://mastodon.technology/users/deleteduser\",\"signature\": {\"type\": \"RsaSignature2017\",\"creator\": \"https://mastodon.technology/users/deleteduser#main-key\",\"created\": \"2020-11-19T22:43:01Z\",\"signatureValue\": \"peksQao4v5N+sMZgHXZ6xZnGaZrd0s+LqZimu63cnp7O5NBJM6gY9AAu/vKUgrh4C50r66f9OQdHg5yChQhc4ViE+yLR/3/e59YQimelmXJPpcC99Nt0YLU/iTRLsBehY3cDdC6+ogJKgpkToQvB6tG2KrPdrkreYh4Il4eXLKMfiQhgdKluOvenLnl2erPWfE02hIu/jpuljyxSuvJunMdU4yQVSZHTtk/I8q3jjzIzhgyb7ICWU5Hkx0H/47Q24ztsvOgiTWNgO+v6l9vA7qIhztENiRPhzGP5RCCzUKRAe6bcSu1Wfa3NKWqB9BeJ7s+2y2bD7ubPbiEE1MQV7Q==\"}}";
var data = ApDeserializer.ProcessActivity(json) as ActivityDelete;
Assert.AreEqual("https://mastodon.technology/users/deleteduser#delete", data.id);
Assert.AreEqual("Delete", data.type);
Assert.AreEqual("https://mastodon.technology/users/deleteduser", data.actor);
Assert.AreEqual("https://mastodon.technology/users/deleteduser", data.apObject);
}
//[TestMethod]
//public void NoteDeserializationTest()
//{

View File

@ -1,20 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="MSTest.TestAdapter" Version="2.1.0" />
<PackageReference Include="MSTest.TestFramework" Version="2.1.0" />
<PackageReference Include="coverlet.collector" Version="1.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.0.3" />
<PackageReference Include="MSTest.TestFramework" Version="3.0.3" />
<PackageReference Include="coverlet.collector" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\BirdsiteLive.ActivityPub\BirdsiteLive.ActivityPub.csproj" />
</ItemGroup>
</Project>
</Project>

View File

@ -1,20 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="MSTest.TestAdapter" Version="2.1.1" />
<PackageReference Include="MSTest.TestFramework" Version="2.1.1" />
<PackageReference Include="coverlet.collector" Version="1.3.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.0.3" />
<PackageReference Include="MSTest.TestFramework" Version="3.0.3" />
<PackageReference Include="coverlet.collector" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\BirdsiteLive.Common\BirdsiteLive.Common.csproj" />
</ItemGroup>
</Project>
</Project>

View File

@ -0,0 +1,72 @@
using BirdsiteLive.Common.Regexes;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace BirdsiteLive.Common.Tests
{
[TestClass]
public class UrlRegexesTests
{
[TestMethod]
public void Url_Test()
{
var input = "https://misskey.tdl/users/8hwf6zy2k1#main-key";
Assert.IsTrue(UrlRegexes.Url.IsMatch(input));
}
[TestMethod]
public void Url_Not_Test()
{
var input = "misskey.tdl/users/8hwf6zy2k1#main-key";
Assert.IsFalse(UrlRegexes.Url.IsMatch(input));
}
[TestMethod]
public void Domain_Test()
{
var input = "misskey-data_sq.tdl";
Assert.IsTrue(UrlRegexes.Domain.IsMatch(input));
}
[TestMethod]
public void Domain_Numbers_Test()
{
var input = "miss45654QAzedqskey-data_sq.tdl";
Assert.IsTrue(UrlRegexes.Domain.IsMatch(input));
}
[TestMethod]
public void Domain_Subdomain_Test()
{
var input = "s.sub.dqdq-_Dz9sd.tdl";
Assert.IsTrue(UrlRegexes.Domain.IsMatch(input));
}
[TestMethod]
public void Domain_Not_Test()
{
var input = "mis$s45654QAzedqskey-data_sq.tdl";
Assert.IsFalse(UrlRegexes.Domain.IsMatch(input));
}
[TestMethod]
public void Domain_Slash_Test()
{
var input = "miss45654QAz/edqskey-data_sq.tdl";
Assert.IsFalse(UrlRegexes.Domain.IsMatch(input));
}
[TestMethod]
public void Domain_NotSub_Test()
{
var input = ".mis$s45654QAzedqskey-data_sq.tdl";
Assert.IsFalse(UrlRegexes.Domain.IsMatch(input));
}
[TestMethod]
public void Domain_NotExt_Test()
{
var input = ".mis$s45654QAzedqskey-data_sq.tdl";
Assert.IsFalse(UrlRegexes.Domain.IsMatch(input));
}
}
}

View File

@ -1,20 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="MSTest.TestAdapter" Version="2.1.0" />
<PackageReference Include="MSTest.TestFramework" Version="2.1.0" />
<PackageReference Include="coverlet.collector" Version="1.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.0.3" />
<PackageReference Include="MSTest.TestFramework" Version="3.0.3" />
<PackageReference Include="coverlet.collector" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\BirdsiteLive.Cryptography\BirdsiteLive.Cryptography.csproj" />
</ItemGroup>
</Project>
</Project>

View File

@ -1,16 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="MSTest.TestAdapter" Version="2.1.0" />
<PackageReference Include="MSTest.TestFramework" Version="2.1.0" />
<PackageReference Include="coverlet.collector" Version="1.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.0.3" />
<PackageReference Include="MSTest.TestFramework" Version="3.0.3" />
<PackageReference Include="coverlet.collector" Version="6.0.0" />
</ItemGroup>
<ItemGroup>

View File

@ -14,7 +14,7 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers.Base
{
_settings = new PostgresSettings
{
ConnString = "Host=127.0.0.1;Username=postgres;Password=mysecretpassword;Database=mytestdb",
ConnString = "Host=127.0.0.1;Username=birdtest;Password=mysecretpassword;Database=birdsitetest",
DbVersionTableName = "DbVersionTableName" + RandomGenerator.GetString(4),
CachedTweetsTableName = "CachedTweetsTableName" + RandomGenerator.GetString(4),
FollowersTableName = "FollowersTableName" + RandomGenerator.GetString(4),

View File

@ -340,6 +340,48 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
Assert.AreEqual(10, result.PostingErrorCount);
}
[TestMethod]
public async Task CreateUpdateAndGetFollower_Integer()
{
var acct = "myhandle";
var host = "domain.ext";
var following = new[] { 12, 19, 23 };
var followingSync = new Dictionary<int, long>()
{
{12, 165L},
{19, 166L},
{23, 167L}
};
var inboxRoute = "/myhandle/inbox";
var sharedInboxRoute = "/inbox";
var actorId = $"https://{host}/{acct}";
var dal = new FollowersPostgresDal(_settings);
await dal.CreateFollowerAsync(acct, host, inboxRoute, sharedInboxRoute, actorId, following, followingSync);
var result = await dal.GetFollowerAsync(acct, host);
var updatedFollowing = new List<int> { 12, 19, 23, 24 };
var updatedFollowingSync = new Dictionary<int, long>(){
{12, 170L},
{19, 171L},
{23, 172L},
{24, 173L}
};
result.Followings = updatedFollowing.ToList();
result.FollowingsSyncStatus = updatedFollowingSync;
result.PostingErrorCount = 32768;
await dal.UpdateFollowerAsync(result);
result = await dal.GetFollowerAsync(acct, host);
Assert.AreEqual(updatedFollowing.Count, result.Followings.Count);
Assert.AreEqual(updatedFollowing[0], result.Followings[0]);
Assert.AreEqual(updatedFollowingSync.Count, result.FollowingsSyncStatus.Count);
Assert.AreEqual(updatedFollowingSync.First().Key, result.FollowingsSyncStatus.First().Key);
Assert.AreEqual(updatedFollowingSync.First().Value, result.FollowingsSyncStatus.First().Value);
Assert.AreEqual(32768, result.PostingErrorCount);
}
[TestMethod]
public async Task CreateUpdateAndGetFollower_Remove()
{

View File

@ -71,6 +71,28 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
Assert.AreEqual(result.Id, resultById.Id);
}
[TestMethod]
public async Task CreateAndGetMigratedUser_byId()
{
var acct = "myid";
var lastTweetId = 1548L;
var movedTo = "https://";
var movedToAcct = "@account@instance";
var dal = new TwitterUserPostgresDal(_settings);
await dal.CreateTwitterUserAsync(acct, lastTweetId, movedTo, movedToAcct);
var result = await dal.GetTwitterUserAsync(acct);
var resultById = await dal.GetTwitterUserAsync(result.Id);
Assert.AreEqual(acct, resultById.Acct);
Assert.AreEqual(lastTweetId, resultById.LastTweetPostedId);
Assert.AreEqual(lastTweetId, resultById.LastTweetSynchronizedForAllFollowersId);
Assert.AreEqual(result.Id, resultById.Id);
Assert.AreEqual(movedTo, result.MovedTo);
Assert.AreEqual(movedToAcct, result.MovedToAcct);
}
[TestMethod]
public async Task CreateUpdateAndGetUser()
{
@ -87,7 +109,7 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
var updatedLastSyncId = 1550L;
var now = DateTime.Now;
var errors = 15;
await dal.UpdateTwitterUserAsync(result.Id, updatedLastTweetId, updatedLastSyncId, errors, now);
await dal.UpdateTwitterUserAsync(result.Id, updatedLastTweetId, updatedLastSyncId, errors, now, null, null, false);
result = await dal.GetTwitterUserAsync(acct);
@ -96,6 +118,68 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
Assert.AreEqual(updatedLastSyncId, result.LastTweetSynchronizedForAllFollowersId);
Assert.AreEqual(errors, result.FetchingErrorCount);
Assert.IsTrue(Math.Abs((now.ToUniversalTime() - result.LastSync).Milliseconds) < 100);
Assert.AreEqual(null, result.MovedTo);
Assert.AreEqual(null, result.MovedToAcct);
}
[TestMethod]
public async Task CreateUpdateAndGetMigratedUser()
{
var acct = "myid";
var lastTweetId = 1548L;
var dal = new TwitterUserPostgresDal(_settings);
await dal.CreateTwitterUserAsync(acct, lastTweetId);
var result = await dal.GetTwitterUserAsync(acct);
var updatedLastTweetId = 1600L;
var updatedLastSyncId = 1550L;
var now = DateTime.Now;
var errors = 15;
var movedTo = "https://";
var movedToAcct = "@account@instance";
await dal.UpdateTwitterUserAsync(result.Id, updatedLastTweetId, updatedLastSyncId, errors, now, movedTo, movedToAcct, false);
result = await dal.GetTwitterUserAsync(acct);
Assert.AreEqual(acct, result.Acct);
Assert.AreEqual(updatedLastTweetId, result.LastTweetPostedId);
Assert.AreEqual(updatedLastSyncId, result.LastTweetSynchronizedForAllFollowersId);
Assert.AreEqual(errors, result.FetchingErrorCount);
Assert.IsTrue(Math.Abs((now.ToUniversalTime() - result.LastSync).Milliseconds) < 100);
Assert.AreEqual(movedTo, result.MovedTo);
Assert.AreEqual(movedToAcct, result.MovedToAcct);
}
[TestMethod]
public async Task CreateUpdateAndGetDeletedUser()
{
var acct = "myid";
var lastTweetId = 1548L;
var dal = new TwitterUserPostgresDal(_settings);
await dal.CreateTwitterUserAsync(acct, lastTweetId);
var result = await dal.GetTwitterUserAsync(acct);
var updatedLastTweetId = 1600L;
var updatedLastSyncId = 1550L;
var now = DateTime.Now;
var errors = 15;
await dal.UpdateTwitterUserAsync(result.Id, updatedLastTweetId, updatedLastSyncId, errors, now, null, null, true);
result = await dal.GetTwitterUserAsync(acct);
Assert.AreEqual(acct, result.Acct);
Assert.AreEqual(updatedLastTweetId, result.LastTweetPostedId);
Assert.AreEqual(updatedLastSyncId, result.LastTweetSynchronizedForAllFollowersId);
Assert.AreEqual(errors, result.FetchingErrorCount);
Assert.IsTrue(Math.Abs((now.ToUniversalTime() - result.LastSync).Milliseconds) < 100);
Assert.AreEqual(null, result.MovedTo);
Assert.AreEqual(null, result.MovedToAcct);
Assert.AreEqual(true, result.Deleted);
}
[TestMethod]
@ -130,12 +214,44 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
Assert.IsTrue(Math.Abs((now.ToUniversalTime() - result.LastSync).Milliseconds) < 100);
}
[TestMethod]
public async Task CreateUpdate3AndGetUser()
{
var acct = "myid";
var lastTweetId = 1548L;
var dal = new TwitterUserPostgresDal(_settings);
await dal.CreateTwitterUserAsync(acct, lastTweetId);
var result = await dal.GetTwitterUserAsync(acct);
var updatedLastTweetId = 1600L;
var updatedLastSyncId = 1550L;
var now = DateTime.Now;
var errors = 32768;
result.LastTweetPostedId = updatedLastTweetId;
result.LastTweetSynchronizedForAllFollowersId = updatedLastSyncId;
result.FetchingErrorCount = errors;
result.LastSync = now;
await dal.UpdateTwitterUserAsync(result);
result = await dal.GetTwitterUserAsync(acct);
Assert.AreEqual(acct, result.Acct);
Assert.AreEqual(updatedLastTweetId, result.LastTweetPostedId);
Assert.AreEqual(updatedLastSyncId, result.LastTweetSynchronizedForAllFollowersId);
Assert.AreEqual(errors, result.FetchingErrorCount);
Assert.IsTrue(Math.Abs((now.ToUniversalTime() - result.LastSync).Milliseconds) < 100);
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public async Task Update_NoId()
{
var dal = new TwitterUserPostgresDal(_settings);
await dal.UpdateTwitterUserAsync(default, default, default, default, DateTime.UtcNow);
await dal.UpdateTwitterUserAsync(default, default, default, default, DateTime.UtcNow, null, null, false);
}
[TestMethod]
@ -143,7 +259,7 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
public async Task Update_NoLastTweetPostedId()
{
var dal = new TwitterUserPostgresDal(_settings);
await dal.UpdateTwitterUserAsync(12, default, default, default, DateTime.UtcNow);
await dal.UpdateTwitterUserAsync(12, default, default, default, DateTime.UtcNow, null, null, false);
}
[TestMethod]
@ -151,7 +267,7 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
public async Task Update_NoLastTweetSynchronizedForAllFollowersId()
{
var dal = new TwitterUserPostgresDal(_settings);
await dal.UpdateTwitterUserAsync(12, 9556, default, default, DateTime.UtcNow);
await dal.UpdateTwitterUserAsync(12, 9556, default, default, DateTime.UtcNow, null, null, false);
}
[TestMethod]
@ -159,7 +275,7 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
public async Task Update_NoLastSync()
{
var dal = new TwitterUserPostgresDal(_settings);
await dal.UpdateTwitterUserAsync(12, 9556, 65, default, default);
await dal.UpdateTwitterUserAsync(12, 9556, 65, default, default, null, null, false);
}
[TestMethod]
@ -224,12 +340,79 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
await dal.CreateTwitterUserAsync(acct, lastTweetId);
}
var result = await dal.GetAllTwitterUsersAsync(1000);
for (int i = 0; i < 10; i++)
{
var acct = $"migrated-myid{i}";
var lastTweetId = 1548L;
await dal.CreateTwitterUserAsync(acct, lastTweetId, "https://url/account", "@user@domain");
}
for (int i = 0; i < 10; i++)
{
var acct = $"deleted-myid{i}";
var lastTweetId = 148L;
await dal.CreateTwitterUserAsync(acct, lastTweetId);
var user = await dal.GetTwitterUserAsync(acct);
user.Deleted = true;
user.LastSync = DateTime.UtcNow;
await dal.UpdateTwitterUserAsync(user);
}
var result = await dal.GetAllTwitterUsersAsync(1100, false);
Assert.AreEqual(1000, result.Length);
Assert.IsFalse(result[0].Id == default);
Assert.IsFalse(result[0].Acct == default);
Assert.IsFalse(result[0].LastTweetPostedId == default);
Assert.IsFalse(result[0].LastTweetSynchronizedForAllFollowersId == default);
foreach (var user in result)
{
Assert.IsTrue(string.IsNullOrWhiteSpace(user.MovedTo));
Assert.IsTrue(string.IsNullOrWhiteSpace(user.MovedToAcct));
Assert.IsFalse(user.Deleted);
}
}
[TestMethod]
public async Task GetAllTwitterUsers_Top_RetrieveDeleted()
{
var dal = new TwitterUserPostgresDal(_settings);
for (var i = 0; i < 1000; i++)
{
var acct = $"myid{i}";
var lastTweetId = 1548L;
await dal.CreateTwitterUserAsync(acct, lastTweetId);
}
for (int i = 0; i < 10; i++)
{
var acct = $"migrated-myid{i}";
var lastTweetId = 1548L;
await dal.CreateTwitterUserAsync(acct, lastTweetId, "https://url/account", "@user@domain");
}
for (int i = 0; i < 10; i++)
{
var acct = $"deleted-myid{i}";
var lastTweetId = 148L;
await dal.CreateTwitterUserAsync(acct, lastTweetId);
var user = await dal.GetTwitterUserAsync(acct);
user.Deleted = true;
user.LastSync = DateTime.UtcNow;
await dal.UpdateTwitterUserAsync(user);
}
var result = await dal.GetAllTwitterUsersAsync(1100, true);
Assert.AreEqual(1020, result.Length);
Assert.IsFalse(result[0].Id == default);
Assert.IsFalse(result[0].Acct == default);
Assert.IsFalse(result[0].LastTweetPostedId == default);
Assert.IsFalse(result[0].LastTweetSynchronizedForAllFollowersId == default);
}
[TestMethod]
@ -247,7 +430,7 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
// Update accounts
var now = DateTime.UtcNow;
var allUsers = await dal.GetAllTwitterUsersAsync();
var allUsers = await dal.GetAllTwitterUsersAsync(false);
foreach (var acc in allUsers)
{
var lastSync = now.AddDays(acc.LastTweetPostedId);
@ -258,7 +441,7 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
// Create a not init account
await dal.CreateTwitterUserAsync("not_init", -1);
var result = await dal.GetAllTwitterUsersAsync(10);
var result = await dal.GetAllTwitterUsersAsync(10, false);
Assert.IsTrue(result.Any(x => x.Acct == "myid0"));
Assert.IsTrue(result.Any(x => x.Acct == "myid8"));
@ -281,15 +464,15 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
await dal.CreateTwitterUserAsync(acct, lastTweetId);
}
var allUsers = await dal.GetAllTwitterUsersAsync(100);
var allUsers = await dal.GetAllTwitterUsersAsync(100, false);
for (var i = 0; i < 20; i++)
{
var user = allUsers[i];
var date = i % 2 == 0 ? oldest : newest;
await dal.UpdateTwitterUserAsync(user.Id, user.LastTweetPostedId, user.LastTweetSynchronizedForAllFollowersId, 0, date);
await dal.UpdateTwitterUserAsync(user.Id, user.LastTweetPostedId, user.LastTweetSynchronizedForAllFollowersId, 0, date, null, null, false);
}
var result = await dal.GetAllTwitterUsersAsync(10);
var result = await dal.GetAllTwitterUsersAsync(10, false);
Assert.AreEqual(10, result.Length);
Assert.IsFalse(result[0].Id == default);
Assert.IsFalse(result[0].Acct == default);
@ -312,7 +495,15 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
await dal.CreateTwitterUserAsync(acct, lastTweetId);
}
var result = await dal.GetAllTwitterUsersAsync();
for (int i = 0; i < 10; i++)
{
var acct = $"migrated-myid{i}";
var lastTweetId = 1548L;
await dal.CreateTwitterUserAsync(acct, lastTweetId, "https://url/account", "@user@domain");
}
var result = await dal.GetAllTwitterUsersAsync(false);
Assert.AreEqual(1000, result.Length);
Assert.IsFalse(result[0].Id == default);
Assert.IsFalse(result[0].Acct == default);
@ -350,7 +541,7 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
if (i == 0 || i == 2 || i == 3)
{
var t = await dal.GetTwitterUserAsync(acct);
await dal.UpdateTwitterUserAsync(t.Id ,1L,2L, 50+i*2, DateTime.Now);
await dal.UpdateTwitterUserAsync(t.Id ,1L,2L, 50+i*2, DateTime.Now, null, null, false);
}
}

View File

@ -1,21 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="Moq" Version="4.14.5" />
<PackageReference Include="MSTest.TestAdapter" Version="2.1.0" />
<PackageReference Include="MSTest.TestFramework" Version="2.1.0" />
<PackageReference Include="coverlet.collector" Version="1.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="MSTest.TestAdapter" Version="3.0.3" />
<PackageReference Include="MSTest.TestFramework" Version="3.0.3" />
<PackageReference Include="coverlet.collector" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\DataAccessLayers\BirdsiteLive.DAL\BirdsiteLive.DAL.csproj" />
</ItemGroup>
</Project>
</Project>

View File

@ -1,17 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="Moq" Version="4.14.5" />
<PackageReference Include="MSTest.TestAdapter" Version="2.1.0" />
<PackageReference Include="MSTest.TestFramework" Version="2.1.0" />
<PackageReference Include="coverlet.collector" Version="1.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="MSTest.TestAdapter" Version="3.0.3" />
<PackageReference Include="MSTest.TestFramework" Version="3.0.3" />
<PackageReference Include="coverlet.collector" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
@ -22,4 +22,4 @@
<Folder Include="Repository\" />
</ItemGroup>
</Project>
</Project>

View File

@ -0,0 +1,97 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using BirdsiteLive.DAL.Contracts;
using BirdsiteLive.DAL.Models;
using BirdsiteLive.Domain.BusinessUseCases;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
namespace BirdsiteLive.Domain.Tests.BusinessUseCases
{
[TestClass]
public class ProcessDeleteUserTests
{
[TestMethod]
public async Task ExecuteAsync_NoMoreFollowings()
{
#region Stubs
var follower = new Follower
{
Id = 12,
Followings = new List<int> { 1 }
};
#endregion
#region Mocks
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
followersDalMock
.Setup(x => x.GetFollowersAsync(
It.Is<int>(y => y == 1)))
.ReturnsAsync(new[] { follower });
followersDalMock
.Setup(x => x.DeleteFollowerAsync(
It.Is<int>(y => y == 12)))
.Returns(Task.CompletedTask);
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
twitterUserDalMock
.Setup(x => x.DeleteTwitterUserAsync(
It.Is<int>(y => y == 1)))
.Returns(Task.CompletedTask);
#endregion
var action = new ProcessDeleteUser(followersDalMock.Object, twitterUserDalMock.Object);
await action.ExecuteAsync(follower);
#region Validations
followersDalMock.VerifyAll();
twitterUserDalMock.VerifyAll();
#endregion
}
[TestMethod]
public async Task ExecuteAsync_HaveFollowings()
{
#region Stubs
var follower = new Follower
{
Id = 12,
Followings = new List<int> { 1 }
};
var followers = new List<Follower>
{
follower,
new Follower
{
Id = 11
}
};
#endregion
#region Mocks
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
followersDalMock
.Setup(x => x.GetFollowersAsync(
It.Is<int>(y => y == 1)))
.ReturnsAsync(followers.ToArray());
followersDalMock
.Setup(x => x.DeleteFollowerAsync(
It.Is<int>(y => y == 12)))
.Returns(Task.CompletedTask);
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
#endregion
var action = new ProcessDeleteUser(followersDalMock.Object, twitterUserDalMock.Object);
await action.ExecuteAsync(follower);
#region Validations
followersDalMock.VerifyAll();
twitterUserDalMock.VerifyAll();
#endregion
}
}
}

View File

@ -77,7 +77,9 @@ namespace BirdsiteLive.Domain.Tests.BusinessUseCases
twitterUserDalMock
.Setup(x => x.CreateTwitterUserAsync(
It.Is<string>(y => y == twitterName),
It.Is<long>(y => y == -1)))
It.Is<long>(y => y == -1),
It.Is<string>(y => y == null),
It.Is<string>(y => y == null)))
.Returns(Task.CompletedTask);
#endregion

View File

@ -0,0 +1,36 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using BirdsiteLive.Common.Regexes;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
namespace BirdsiteLive.Domain.Tests
{
[TestClass]
public class TheFedInfoServiceTests
{
[TestMethod]
public async Task GetBslInstanceListAsyncTest()
{
var httpClientFactoryMock = new Mock<IHttpClientFactory>();
httpClientFactoryMock
.Setup(x => x.CreateClient(It.IsAny<string>()))
.Returns(new HttpClient());
var service = new TheFedInfoService(httpClientFactoryMock.Object);
var bslInstanceList = await service.GetBslInstanceListAsync();
Assert.IsTrue(bslInstanceList.Count > 0);
foreach (var instanceInfo in bslInstanceList)
{
Assert.IsFalse(string.IsNullOrWhiteSpace(instanceInfo.Host));
Assert.IsTrue(UrlRegexes.Domain.IsMatch(instanceInfo.Host));
Assert.IsTrue(instanceInfo.Version > new Version(0, 1, 0));
}
}
}
}

View File

@ -2,6 +2,7 @@
using System.Threading.Tasks;
using BirdsiteLive.DAL.Contracts;
using BirdsiteLive.DAL.Models;
using BirdsiteLive.Domain.BusinessUseCases;
using BirdsiteLive.Moderation.Actions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
@ -29,31 +30,19 @@ namespace BirdsiteLive.Moderation.Tests.Actions
It.Is<Follower>(y => y.Id == follower.Id)))
.Returns(Task.CompletedTask);
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
followersDalMock
.Setup(x => x.GetFollowersAsync(
It.Is<int>(y => y == 1)))
.ReturnsAsync(new[] {follower});
followersDalMock
.Setup(x => x.DeleteFollowerAsync(
It.Is<int>(y => y == 12)))
.Returns(Task.CompletedTask);
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
twitterUserDalMock
.Setup(x => x.DeleteTwitterUserAsync(
It.Is<int>(y => y == 1)))
var processDeleteUserMock = new Mock<IProcessDeleteUser>(MockBehavior.Strict);
processDeleteUserMock
.Setup(x => x.ExecuteAsync(
It.Is<Follower>(y => y.Id == follower.Id)))
.Returns(Task.CompletedTask);
#endregion
var action = new RemoveFollowerAction(followersDalMock.Object, twitterUserDalMock.Object, rejectAllFollowingsActionMock.Object);
var action = new RemoveFollowerAction(rejectAllFollowingsActionMock.Object, processDeleteUserMock.Object);
await action.ProcessAsync(follower);
#region Validations
followersDalMock.VerifyAll();
twitterUserDalMock.VerifyAll();
rejectAllFollowingsActionMock.VerifyAll();
processDeleteUserMock.VerifyAll();
#endregion
}
@ -66,15 +55,6 @@ namespace BirdsiteLive.Moderation.Tests.Actions
Id = 12,
Followings = new List<int> { 1 }
};
var followers = new List<Follower>
{
follower,
new Follower
{
Id = 11
}
};
#endregion
#region Mocks
@ -84,27 +64,19 @@ namespace BirdsiteLive.Moderation.Tests.Actions
It.Is<Follower>(y => y.Id == follower.Id)))
.Returns(Task.CompletedTask);
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
followersDalMock
.Setup(x => x.GetFollowersAsync(
It.Is<int>(y => y == 1)))
.ReturnsAsync(followers.ToArray());
followersDalMock
.Setup(x => x.DeleteFollowerAsync(
It.Is<int>(y => y == 12)))
var processDeleteUserMock = new Mock<IProcessDeleteUser>(MockBehavior.Strict);
processDeleteUserMock
.Setup(x => x.ExecuteAsync(
It.Is<Follower>(y => y.Id == follower.Id)))
.Returns(Task.CompletedTask);
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
#endregion
var action = new RemoveFollowerAction(followersDalMock.Object, twitterUserDalMock.Object, rejectAllFollowingsActionMock.Object);
var action = new RemoveFollowerAction(rejectAllFollowingsActionMock.Object, processDeleteUserMock.Object);
await action.ProcessAsync(follower);
#region Validations
followersDalMock.VerifyAll();
twitterUserDalMock.VerifyAll();
rejectAllFollowingsActionMock.VerifyAll();
processDeleteUserMock.VerifyAll();
#endregion
}
}

View File

@ -1,21 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="Moq" Version="4.14.5" />
<PackageReference Include="MSTest.TestAdapter" Version="2.1.1" />
<PackageReference Include="MSTest.TestFramework" Version="2.1.1" />
<PackageReference Include="coverlet.collector" Version="1.3.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="MSTest.TestAdapter" Version="3.0.3" />
<PackageReference Include="MSTest.TestFramework" Version="3.0.3" />
<PackageReference Include="coverlet.collector" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\BirdsiteLive.Moderation\BirdsiteLive.Moderation.csproj" />
</ItemGroup>
</Project>
</Project>

View File

@ -48,7 +48,7 @@ namespace BirdsiteLive.Moderation.Tests.Processors
#region Mocks
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
twitterUserDalMock
.Setup(x => x.GetAllTwitterUsersAsync())
.Setup(x => x.GetAllTwitterUsersAsync(It.Is<bool>(y => y == false)))
.ReturnsAsync(allUsers.ToArray());
var moderationRepositoryMock = new Mock<IModerationRepository>(MockBehavior.Strict);
@ -87,7 +87,7 @@ namespace BirdsiteLive.Moderation.Tests.Processors
#region Mocks
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
twitterUserDalMock
.Setup(x => x.GetAllTwitterUsersAsync())
.Setup(x => x.GetAllTwitterUsersAsync(It.Is<bool>(y => y == false)))
.ReturnsAsync(allUsers.ToArray());
var moderationRepositoryMock = new Mock<IModerationRepository>(MockBehavior.Strict);
@ -130,7 +130,7 @@ namespace BirdsiteLive.Moderation.Tests.Processors
#region Mocks
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
twitterUserDalMock
.Setup(x => x.GetAllTwitterUsersAsync())
.Setup(x => x.GetAllTwitterUsersAsync(It.Is<bool>(y => y == false)))
.ReturnsAsync(allUsers.ToArray());
var moderationRepositoryMock = new Mock<IModerationRepository>(MockBehavior.Strict);
@ -173,7 +173,7 @@ namespace BirdsiteLive.Moderation.Tests.Processors
#region Mocks
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
twitterUserDalMock
.Setup(x => x.GetAllTwitterUsersAsync())
.Setup(x => x.GetAllTwitterUsersAsync(It.Is<bool>(y => y == false)))
.ReturnsAsync(allUsers.ToArray());
var moderationRepositoryMock = new Mock<IModerationRepository>(MockBehavior.Strict);

View File

@ -1,17 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="Moq" Version="4.14.5" />
<PackageReference Include="MSTest.TestAdapter" Version="2.1.0" />
<PackageReference Include="MSTest.TestFramework" Version="2.1.0" />
<PackageReference Include="coverlet.collector" Version="1.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="MSTest.TestAdapter" Version="3.0.3" />
<PackageReference Include="MSTest.TestFramework" Version="3.0.3" />
<PackageReference Include="coverlet.collector" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
@ -24,4 +24,4 @@
<ProjectReference Include="..\..\DataAccessLayers\BirdsiteLive.DAL\BirdsiteLive.DAL.csproj" />
</ItemGroup>
</Project>
</Project>

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@ -159,11 +160,136 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
twitterUserServiceMock
.Setup(x => x.GetUser(It.Is<string>(y => y == acct2)))
.Returns((TwitterUser) null);
.Throws(new UserNotFoundException());
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
var removeTwitterAccountActionMock = new Mock<IRemoveTwitterAccountAction>(MockBehavior.Strict);
removeTwitterAccountActionMock
.Setup(x => x.ProcessAsync(It.Is<SyncTwitterUser>(y => y.Acct == acct2)))
.Returns(Task.CompletedTask);
#endregion
var processor = new RefreshTwitterUserStatusProcessor(twitterUserServiceMock.Object, twitterUserDalMock.Object, removeTwitterAccountActionMock.Object, settings);
var result = await processor.ProcessAsync(users.ToArray(), CancellationToken.None);
#region Validations
Assert.AreEqual(1, result.Length);
Assert.IsTrue(result.Any(x => x.User.Id == userId1));
twitterUserServiceMock.VerifyAll();
twitterUserDalMock.VerifyAll();
removeTwitterAccountActionMock.VerifyAll();
#endregion
}
[TestMethod]
public async Task ProcessAsync_Suspended_Test()
{
#region Stubs
var userId1 = 1;
var acct1 = "user1";
var userId2 = 2;
var acct2 = "user2";
var users = new List<SyncTwitterUser>
{
new SyncTwitterUser
{
Id = userId1,
Acct = acct1
},
new SyncTwitterUser
{
Id = userId2,
Acct = acct2
}
};
var settings = new InstanceSettings
{
FailingTwitterUserCleanUpThreshold = 300
};
#endregion
#region Mocks
var twitterUserServiceMock = new Mock<ICachedTwitterUserService>(MockBehavior.Strict);
twitterUserServiceMock
.Setup(x => x.GetUser(It.Is<string>(y => y == acct1)))
.Returns(new TwitterUser
{
Protected = false
});
twitterUserServiceMock
.Setup(x => x.PurgeUser(It.Is<string>(y => y == acct2)));
.Setup(x => x.GetUser(It.Is<string>(y => y == acct2)))
.Throws(new UserHasBeenSuspendedException());
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
var removeTwitterAccountActionMock = new Mock<IRemoveTwitterAccountAction>(MockBehavior.Strict);
removeTwitterAccountActionMock
.Setup(x => x.ProcessAsync(It.Is<SyncTwitterUser>(y => y.Acct == acct2)))
.Returns(Task.CompletedTask);
#endregion
var processor = new RefreshTwitterUserStatusProcessor(twitterUserServiceMock.Object, twitterUserDalMock.Object, removeTwitterAccountActionMock.Object, settings);
var result = await processor.ProcessAsync(users.ToArray(), CancellationToken.None);
#region Validations
Assert.AreEqual(1, result.Length);
Assert.IsTrue(result.Any(x => x.User.Id == userId1));
twitterUserServiceMock.VerifyAll();
twitterUserDalMock.VerifyAll();
removeTwitterAccountActionMock.VerifyAll();
#endregion
}
[TestMethod]
public async Task ProcessAsync_Exception_Test()
{
#region Stubs
var userId1 = 1;
var acct1 = "user1";
var userId2 = 2;
var acct2 = "user2";
var users = new List<SyncTwitterUser>
{
new SyncTwitterUser
{
Id = userId1,
Acct = acct1
},
new SyncTwitterUser
{
Id = userId2,
Acct = acct2
}
};
var settings = new InstanceSettings
{
FailingTwitterUserCleanUpThreshold = 300
};
#endregion
#region Mocks
var twitterUserServiceMock = new Mock<ICachedTwitterUserService>(MockBehavior.Strict);
twitterUserServiceMock
.Setup(x => x.GetUser(It.Is<string>(y => y == acct1)))
.Returns(new TwitterUser
{
Protected = false
});
twitterUserServiceMock
.Setup(x => x.GetUser(It.Is<string>(y => y == acct2)))
.Throws(new Exception());
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
twitterUserDalMock
.Setup(x => x.GetTwitterUserAsync(It.Is<string>(y => y == acct2)))
@ -194,7 +320,7 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
}
[TestMethod]
public async Task ProcessAsync_Unfound_OverThreshold_Test()
public async Task ProcessAsync_Error_Test()
{
#region Stubs
var userId1 = 1;
@ -235,10 +361,79 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
twitterUserServiceMock
.Setup(x => x.GetUser(It.Is<string>(y => y == acct2)))
.Returns((TwitterUser)null);
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
twitterUserDalMock
.Setup(x => x.GetTwitterUserAsync(It.Is<string>(y => y == acct2)))
.ReturnsAsync(new SyncTwitterUser
{
Id = userId2,
FetchingErrorCount = 0
});
twitterUserDalMock
.Setup(x => x.UpdateTwitterUserAsync(It.Is<SyncTwitterUser>(y => y.Id == userId2 && y.FetchingErrorCount == 1)))
.Returns(Task.CompletedTask);
var removeTwitterAccountActionMock = new Mock<IRemoveTwitterAccountAction>(MockBehavior.Strict);
#endregion
var processor = new RefreshTwitterUserStatusProcessor(twitterUserServiceMock.Object, twitterUserDalMock.Object, removeTwitterAccountActionMock.Object, settings);
var result = await processor.ProcessAsync(users.ToArray(), CancellationToken.None);
#region Validations
Assert.AreEqual(1, result.Length);
Assert.IsTrue(result.Any(x => x.User.Id == userId1));
twitterUserServiceMock.VerifyAll();
twitterUserDalMock.VerifyAll();
removeTwitterAccountActionMock.VerifyAll();
#endregion
}
[TestMethod]
public async Task ProcessAsync_Error_OverThreshold_Test()
{
#region Stubs
var userId1 = 1;
var acct1 = "user1";
var userId2 = 2;
var acct2 = "user2";
var users = new List<SyncTwitterUser>
{
new SyncTwitterUser
{
Id = userId1,
Acct = acct1
},
new SyncTwitterUser
{
Id = userId2,
Acct = acct2
}
};
var settings = new InstanceSettings
{
FailingTwitterUserCleanUpThreshold = 300
};
#endregion
#region Mocks
var twitterUserServiceMock = new Mock<ICachedTwitterUserService>(MockBehavior.Strict);
twitterUserServiceMock
.Setup(x => x.GetUser(It.Is<string>(y => y == acct1)))
.Returns(new TwitterUser
{
Protected = false
});
twitterUserServiceMock
.Setup(x => x.PurgeUser(It.Is<string>(y => y == acct2)));
.Setup(x => x.GetUser(It.Is<string>(y => y == acct2)))
.Returns((TwitterUser)null);
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
twitterUserDalMock
.Setup(x => x.GetTwitterUserAsync(It.Is<string>(y => y == acct2)))
@ -312,8 +507,20 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
{
Protected = true
});
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
twitterUserDalMock
.Setup(x => x.GetTwitterUserAsync(It.Is<string>(y => y == acct2)))
.ReturnsAsync(new SyncTwitterUser
{
Id = userId2,
FetchingErrorCount = 0
});
twitterUserDalMock
.Setup(x => x.UpdateTwitterUserAsync(It.Is<SyncTwitterUser>(y => y.Id == userId2 && y.FetchingErrorCount == 1)))
.Returns(Task.CompletedTask);
var removeTwitterAccountActionMock = new Mock<IRemoveTwitterAccountAction>(MockBehavior.Strict);
#endregion
@ -331,7 +538,81 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
}
[TestMethod]
public async Task ProcessAsync_Unfound_NotInit_Test()
public async Task ProcessAsync_Protected_OverThreshold_Test()
{
#region Stubs
var userId1 = 1;
var acct1 = "user1";
var userId2 = 2;
var acct2 = "user2";
var users = new List<SyncTwitterUser>
{
new SyncTwitterUser
{
Id = userId1,
Acct = acct1
},
new SyncTwitterUser
{
Id = userId2,
Acct = acct2
}
};
var settings = new InstanceSettings
{
FailingTwitterUserCleanUpThreshold = 300
};
#endregion
#region Mocks
var twitterUserServiceMock = new Mock<ICachedTwitterUserService>(MockBehavior.Strict);
twitterUserServiceMock
.Setup(x => x.GetUser(It.Is<string>(y => y == acct1)))
.Returns(new TwitterUser
{
Protected = false
});
twitterUserServiceMock
.Setup(x => x.GetUser(It.Is<string>(y => y == acct2)))
.Returns(new TwitterUser
{
Protected = true
});
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
twitterUserDalMock
.Setup(x => x.GetTwitterUserAsync(It.Is<string>(y => y == acct2)))
.ReturnsAsync(new SyncTwitterUser
{
Id = userId2,
FetchingErrorCount = 500
});
var removeTwitterAccountActionMock = new Mock<IRemoveTwitterAccountAction>(MockBehavior.Strict);
removeTwitterAccountActionMock
.Setup(x => x.ProcessAsync(It.Is<SyncTwitterUser>(y => y.Id == userId2)))
.Returns(Task.CompletedTask);
#endregion
var processor = new RefreshTwitterUserStatusProcessor(twitterUserServiceMock.Object, twitterUserDalMock.Object, removeTwitterAccountActionMock.Object, settings);
var result = await processor.ProcessAsync(users.ToArray(), CancellationToken.None);
#region Validations
Assert.AreEqual(1, result.Length);
Assert.IsTrue(result.Any(x => x.User.Id == userId1));
twitterUserServiceMock.VerifyAll();
twitterUserDalMock.VerifyAll();
removeTwitterAccountActionMock.VerifyAll();
#endregion
}
[TestMethod]
public async Task ProcessAsync_Error_NotInit_Test()
{
#region Stubs
var userId1 = 1;
@ -361,9 +642,6 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
.Setup(x => x.GetUser(It.Is<string>(y => y == acct1)))
.Returns((TwitterUser)null);
twitterUserServiceMock
.Setup(x => x.PurgeUser(It.Is<string>(y => y == acct1)));
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
twitterUserDalMock
.Setup(x => x.GetTwitterUserAsync(It.Is<string>(y => y == acct1)))
@ -388,5 +666,77 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
removeTwitterAccountActionMock.VerifyAll();
#endregion
}
[TestMethod]
public async Task ProcessAsync_RateLimited_Test()
{
#region Stubs
var userId1 = 1;
var acct1 = "user1";
var userId2 = 2;
var acct2 = "user2";
var users = new List<SyncTwitterUser>
{
new SyncTwitterUser
{
Id = userId1,
Acct = acct1
},
new SyncTwitterUser
{
Id = userId2,
Acct = acct2
}
};
var settings = new InstanceSettings
{
FailingTwitterUserCleanUpThreshold = 300
};
#endregion
#region Mocks
var twitterUserServiceMock = new Mock<ICachedTwitterUserService>(MockBehavior.Strict);
twitterUserServiceMock
.Setup(x => x.GetUser(It.Is<string>(y => y == acct1)))
.Returns(new TwitterUser
{
Protected = false,
});
twitterUserServiceMock
.Setup(x => x.GetUser(It.Is<string>(y => y == acct2)))
.Throws(new RateLimitExceededException());
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
twitterUserDalMock
.Setup(x => x.GetTwitterUserAsync(It.Is<string>(y => y == acct2)))
.ReturnsAsync(new SyncTwitterUser
{
Id = userId2,
FetchingErrorCount = 20
});
twitterUserDalMock
.Setup(x => x.UpdateTwitterUserAsync(It.Is<SyncTwitterUser>(y => y.Id == userId2 && y.FetchingErrorCount == 20)))
.Returns(Task.CompletedTask);
var removeTwitterAccountActionMock = new Mock<IRemoveTwitterAccountAction>(MockBehavior.Strict);
#endregion
var processor = new RefreshTwitterUserStatusProcessor(twitterUserServiceMock.Object, twitterUserDalMock.Object, removeTwitterAccountActionMock.Object, settings);
var result = await processor.ProcessAsync(users.ToArray(), CancellationToken.None);
#region Validations
Assert.AreEqual(1, result.Length);
Assert.IsTrue(result.Any(x => x.User.Id == userId1));
twitterUserServiceMock.VerifyAll();
twitterUserDalMock.VerifyAll();
removeTwitterAccountActionMock.VerifyAll();
#endregion
}
}
}

View File

@ -64,7 +64,10 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
It.Is<long>(y => y == tweets.Last().Id),
It.Is<long>(y => y == tweets.Last().Id),
It.Is<int>(y => y == 0),
It.IsAny<DateTime>()
It.IsAny<DateTime>(),
It.Is<string>(y => y == null),
It.Is<string>(y => y == null),
It.Is<bool>(y => y == false)
))
.Returns(Task.CompletedTask);

View File

@ -40,7 +40,8 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
twitterUserDalMock
.Setup(x => x.GetAllTwitterUsersAsync(
It.Is<int>(y => y == maxUsers)))
It.Is<int>(y => y == maxUsers),
It.Is<bool>(y => y == false)))
.ReturnsAsync(users);
var loggerMock = new Mock<ILogger<RetrieveTwitterUsersProcessor>>();
@ -83,7 +84,8 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
twitterUserDalMock
.SetupSequence(x => x.GetAllTwitterUsersAsync(
It.Is<int>(y => y == maxUsers)))
It.Is<int>(y => y == maxUsers),
It.Is<bool>(y => y == false)))
.ReturnsAsync(users.ToArray())
.ReturnsAsync(new SyncTwitterUser[0])
.ReturnsAsync(new SyncTwitterUser[0])
@ -130,7 +132,8 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
twitterUserDalMock
.SetupSequence(x => x.GetAllTwitterUsersAsync(
It.Is<int>(y => y == maxUsers)))
It.Is<int>(y => y == maxUsers),
It.Is<bool>(y => y == false)))
.ReturnsAsync(users.ToArray())
.ReturnsAsync(new SyncTwitterUser[0])
.ReturnsAsync(new SyncTwitterUser[0])
@ -178,7 +181,8 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
twitterUserDalMock
.Setup(x => x.GetAllTwitterUsersAsync(
It.Is<int>(y => y == maxUsers)))
It.Is<int>(y => y == maxUsers),
It.Is<bool>(y => y == false)))
.ReturnsAsync(new SyncTwitterUser[0]);
var loggerMock = new Mock<ILogger<RetrieveTwitterUsersProcessor>>();
@ -215,7 +219,8 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
twitterUserDalMock
.Setup(x => x.GetAllTwitterUsersAsync(
It.Is<int>(y => y == maxUsers)))
It.Is<int>(y => y == maxUsers),
It.Is<bool>(y => y == false)))
.Returns(async () => await DelayFaultedTask<SyncTwitterUser[]>(new Exception()));
var loggerMock = new Mock<ILogger<RetrieveTwitterUsersProcessor>>();

View File

@ -1,16 +1,16 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using BirdsiteLive.DAL.Contracts;
using BirdsiteLive.DAL.Contracts;
using BirdsiteLive.DAL.Models;
using BirdsiteLive.Moderation.Actions;
using BirdsiteLive.Pipeline.Models;
using BirdsiteLive.Pipeline.Processors;
using BirdsiteLive.Twitter.Models;
using Castle.DynamicProxy.Contributors;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace BirdsiteLive.Pipeline.Tests.Processors
{
@ -66,17 +66,93 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
It.Is<long>(y => y == tweet2.Id),
It.Is<long>(y => y == tweet2.Id),
It.Is<int>(y => y == 0),
It.IsAny<DateTime>()
It.IsAny<DateTime>(),
It.Is<string>(y => y == null),
It.Is<string>(y => y == null),
It.Is<bool>(y => y == false)
))
.Returns(Task.CompletedTask);
var removeTwitterAccountActionMock = new Mock<IRemoveTwitterAccountAction>(MockBehavior.Strict);
#endregion
var processor = new SaveProgressionProcessor(twitterUserDalMock.Object, loggerMock.Object);
var processor = new SaveProgressionProcessor(twitterUserDalMock.Object, loggerMock.Object, removeTwitterAccountActionMock.Object);
await processor.ProcessAsync(usersWithTweets, CancellationToken.None);
#region Validations
twitterUserDalMock.VerifyAll();
loggerMock.VerifyAll();
removeTwitterAccountActionMock.VerifyAll();
#endregion
}
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public async Task ProcessAsync_Exception_Test()
{
#region Stubs
var user = new SyncTwitterUser
{
Id = 1
};
var tweet1 = new ExtractedTweet
{
Id = 36
};
var tweet2 = new ExtractedTweet
{
Id = 37
};
var follower1 = new Follower
{
FollowingsSyncStatus = new Dictionary<int, long>
{
{1, 37}
}
};
var usersWithTweets = new UserWithDataToSync
{
Tweets = new[]
{
tweet1,
tweet2
},
Followers = new[]
{
follower1
},
User = user
};
var loggerMock = new Mock<ILogger<SaveProgressionProcessor>>();
#endregion
#region Mocks
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
twitterUserDalMock
.Setup(x => x.UpdateTwitterUserAsync(
It.Is<int>(y => y == user.Id),
It.Is<long>(y => y == tweet2.Id),
It.Is<long>(y => y == tweet2.Id),
It.Is<int>(y => y == 0),
It.IsAny<DateTime>(),
It.Is<string>(y => y == null),
It.Is<string>(y => y == null),
It.Is<bool>(y => y == false)
))
.Throws(new ArgumentException());
var removeTwitterAccountActionMock = new Mock<IRemoveTwitterAccountAction>(MockBehavior.Strict);
#endregion
var processor = new SaveProgressionProcessor(twitterUserDalMock.Object, loggerMock.Object, removeTwitterAccountActionMock.Object);
await processor.ProcessAsync(usersWithTweets, CancellationToken.None);
#region Validations
twitterUserDalMock.VerifyAll();
loggerMock.VerifyAll();
removeTwitterAccountActionMock.VerifyAll();
#endregion
}
@ -132,19 +208,25 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
It.Is<long>(y => y == tweet3.Id),
It.Is<long>(y => y == tweet2.Id),
It.Is<int>(y => y == 0),
It.IsAny<DateTime>()
It.IsAny<DateTime>(),
It.Is<string>(y => y == null),
It.Is<string>(y => y == null),
It.Is<bool>(y => y == false)
))
.Returns(Task.CompletedTask);
var loggerMock = new Mock<ILogger<SaveProgressionProcessor>>();
var removeTwitterAccountActionMock = new Mock<IRemoveTwitterAccountAction>(MockBehavior.Strict);
#endregion
var processor = new SaveProgressionProcessor(twitterUserDalMock.Object, loggerMock.Object);
var processor = new SaveProgressionProcessor(twitterUserDalMock.Object, loggerMock.Object, removeTwitterAccountActionMock.Object);
await processor.ProcessAsync(usersWithTweets, CancellationToken.None);
#region Validations
twitterUserDalMock.VerifyAll();
loggerMock.VerifyAll();
removeTwitterAccountActionMock.VerifyAll();
#endregion
}
@ -208,20 +290,130 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
It.Is<long>(y => y == tweet3.Id),
It.Is<long>(y => y == tweet2.Id),
It.Is<int>(y => y == 0),
It.IsAny<DateTime>()
It.IsAny<DateTime>(),
It.Is<string>(y => y == null),
It.Is<string>(y => y == null),
It.Is<bool>(y => y == false)
))
.Returns(Task.CompletedTask);
var loggerMock = new Mock<ILogger<SaveProgressionProcessor>>();
var removeTwitterAccountActionMock = new Mock<IRemoveTwitterAccountAction>(MockBehavior.Strict);
#endregion
var processor = new SaveProgressionProcessor(twitterUserDalMock.Object, loggerMock.Object);
var processor = new SaveProgressionProcessor(twitterUserDalMock.Object, loggerMock.Object, removeTwitterAccountActionMock.Object);
await processor.ProcessAsync(usersWithTweets, CancellationToken.None);
#region Validations
twitterUserDalMock.VerifyAll();
loggerMock.VerifyAll();
removeTwitterAccountActionMock.VerifyAll();
#endregion
}
[TestMethod]
public async Task ProcessAsync_NoTweets_Test()
{
#region Stubs
var user = new SyncTwitterUser
{
Id = 1,
LastTweetPostedId = 42,
LastSync = DateTime.UtcNow.AddDays(-3)
};
var follower1 = new Follower
{
FollowingsSyncStatus = new Dictionary<int, long>
{
{1, 37}
}
};
var usersWithTweets = new UserWithDataToSync
{
Tweets = Array.Empty<ExtractedTweet>(),
Followers = new[]
{
follower1
},
User = user
};
var loggerMock = new Mock<ILogger<SaveProgressionProcessor>>();
#endregion
#region Mocks
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
twitterUserDalMock
.Setup(x => x.UpdateTwitterUserAsync(
It.Is<SyncTwitterUser>(y => y.LastTweetPostedId == 42
&& y.LastSync > DateTime.UtcNow.AddDays(-1))
))
.Returns(Task.CompletedTask);
var removeTwitterAccountActionMock = new Mock<IRemoveTwitterAccountAction>(MockBehavior.Strict);
#endregion
var processor = new SaveProgressionProcessor(twitterUserDalMock.Object, loggerMock.Object, removeTwitterAccountActionMock.Object);
await processor.ProcessAsync(usersWithTweets, CancellationToken.None);
#region Validations
twitterUserDalMock.VerifyAll();
loggerMock.VerifyAll();
removeTwitterAccountActionMock.VerifyAll();
#endregion
}
[TestMethod]
public async Task ProcessAsync_NoFollower_Test()
{
#region Stubs
var user = new SyncTwitterUser
{
Id = 1
};
var tweet1 = new ExtractedTweet
{
Id = 36
};
var tweet2 = new ExtractedTweet
{
Id = 37
};
var usersWithTweets = new UserWithDataToSync
{
Tweets = new[]
{
tweet1,
tweet2
},
Followers = Array.Empty<Follower>(),
User = user
};
var loggerMock = new Mock<ILogger<SaveProgressionProcessor>>();
#endregion
#region Mocks
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
var removeTwitterAccountActionMock = new Mock<IRemoveTwitterAccountAction>(MockBehavior.Strict);
removeTwitterAccountActionMock
.Setup(x => x.ProcessAsync(It.Is<SyncTwitterUser>(y => y.Id == user.Id)))
.Returns(Task.CompletedTask);
#endregion
var processor = new SaveProgressionProcessor(twitterUserDalMock.Object, loggerMock.Object, removeTwitterAccountActionMock.Object);
await processor.ProcessAsync(usersWithTweets, CancellationToken.None);
#region Validations
twitterUserDalMock.VerifyAll();
loggerMock.VerifyAll();
removeTwitterAccountActionMock.VerifyAll();
#endregion
}
}
}

View File

@ -2,8 +2,10 @@
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using BirdsiteLive.Common.Settings;
using BirdsiteLive.DAL.Contracts;
using BirdsiteLive.DAL.Models;
using BirdsiteLive.Moderation.Actions;
using BirdsiteLive.Pipeline.Models;
using BirdsiteLive.Pipeline.Processors;
using BirdsiteLive.Pipeline.Processors.SubTasks;
@ -72,17 +74,22 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
.Returns(Task.CompletedTask);
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
var settings = new InstanceSettings();
var removeFollowerMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
#endregion
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object);
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object, settings, removeFollowerMock.Object);
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
#region Validations
sendTweetsToInboxTaskMock.VerifyAll();
sendTweetsToSharedInboxTaskMock.VerifyAll();
followersDalMock.VerifyAll();
removeFollowerMock.VerifyAll();
#endregion
}
@ -147,15 +154,20 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
var settings = new InstanceSettings();
var removeFollowerMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
#endregion
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object);
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object, settings, removeFollowerMock.Object);
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
#region Validations
sendTweetsToInboxTaskMock.VerifyAll();
sendTweetsToSharedInboxTaskMock.VerifyAll();
followersDalMock.VerifyAll();
removeFollowerMock.VerifyAll();
#endregion
}
@ -229,15 +241,20 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
.Returns(Task.CompletedTask);
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
var settings = new InstanceSettings();
var removeFollowerMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
#endregion
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object);
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object, settings, removeFollowerMock.Object);
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
#region Validations
sendTweetsToInboxTaskMock.VerifyAll();
sendTweetsToSharedInboxTaskMock.VerifyAll();
followersDalMock.VerifyAll();
removeFollowerMock.VerifyAll();
#endregion
}
@ -312,15 +329,20 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
.Returns(Task.CompletedTask);
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
var settings = new InstanceSettings();
var removeFollowerMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
#endregion
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object);
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object, settings, removeFollowerMock.Object);
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
#region Validations
sendTweetsToInboxTaskMock.VerifyAll();
sendTweetsToSharedInboxTaskMock.VerifyAll();
followersDalMock.VerifyAll();
removeFollowerMock.VerifyAll();
#endregion
}
@ -400,15 +422,20 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
.Returns(Task.CompletedTask);
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
var settings = new InstanceSettings();
var removeFollowerMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
#endregion
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object);
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object, settings, removeFollowerMock.Object);
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
#region Validations
sendTweetsToInboxTaskMock.VerifyAll();
sendTweetsToSharedInboxTaskMock.VerifyAll();
followersDalMock.VerifyAll();
removeFollowerMock.VerifyAll();
#endregion
}
@ -471,15 +498,20 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
var settings = new InstanceSettings();
var removeFollowerMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
#endregion
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object);
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object, settings, removeFollowerMock.Object);
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
#region Validations
sendTweetsToInboxTaskMock.VerifyAll();
sendTweetsToSharedInboxTaskMock.VerifyAll();
followersDalMock.VerifyAll();
removeFollowerMock.VerifyAll();
#endregion
}
@ -543,15 +575,20 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
var settings = new InstanceSettings();
var removeFollowerMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
#endregion
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object);
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object, settings, removeFollowerMock.Object);
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
#region Validations
sendTweetsToInboxTaskMock.VerifyAll();
sendTweetsToSharedInboxTaskMock.VerifyAll();
followersDalMock.VerifyAll();
removeFollowerMock.VerifyAll();
#endregion
}
@ -623,15 +660,196 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
.Returns(Task.CompletedTask);
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
var settings = new InstanceSettings();
var removeFollowerMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
#endregion
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object);
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object, settings, removeFollowerMock.Object);
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
#region Validations
sendTweetsToInboxTaskMock.VerifyAll();
sendTweetsToSharedInboxTaskMock.VerifyAll();
followersDalMock.VerifyAll();
removeFollowerMock.VerifyAll();
#endregion
}
[TestMethod]
public async Task ProcessAsync_MultiInstances_Inbox_OneTweet_Error_SettingsThreshold_Test()
{
#region Stubs
var tweetId = 1;
var host1 = "domain1.ext";
var host2 = "domain2.ext";
var inbox = "/user/inbox";
var userId1 = 2;
var userId2 = 3;
var userAcct = "user";
var userWithTweets = new UserWithDataToSync()
{
Tweets = new[]
{
new ExtractedTweet
{
Id = tweetId
}
},
User = new SyncTwitterUser
{
Acct = userAcct
},
Followers = new[]
{
new Follower
{
Id = userId1,
Host = host1,
InboxRoute = inbox
},
new Follower
{
Id = userId2,
Host = host2,
InboxRoute = inbox,
PostingErrorCount = 42
},
}
};
#endregion
#region Mocks
var sendTweetsToInboxTaskMock = new Mock<ISendTweetsToInboxTask>(MockBehavior.Strict);
sendTweetsToInboxTaskMock
.Setup(x => x.ExecuteAsync(
It.Is<ExtractedTweet[]>(y => y.Length == 1),
It.Is<Follower>(y => y.Id == userId1),
It.Is<SyncTwitterUser>(y => y.Acct == userAcct)))
.Returns(Task.CompletedTask);
sendTweetsToInboxTaskMock
.Setup(x => x.ExecuteAsync(
It.Is<ExtractedTweet[]>(y => y.Length == 1),
It.Is<Follower>(y => y.Id == userId2),
It.Is<SyncTwitterUser>(y => y.Acct == userAcct)))
.Throws(new Exception());
var sendTweetsToSharedInboxTaskMock = new Mock<ISendTweetsToSharedInboxTask>(MockBehavior.Strict);
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
var settings = new InstanceSettings
{
FailingFollowerCleanUpThreshold = 10
};
var removeFollowerMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
removeFollowerMock
.Setup(x => x.ProcessAsync(It.Is<Follower>(y => y.Id == userId2)))
.Returns(Task.CompletedTask);
#endregion
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object, settings, removeFollowerMock.Object);
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
#region Validations
sendTweetsToInboxTaskMock.VerifyAll();
sendTweetsToSharedInboxTaskMock.VerifyAll();
followersDalMock.VerifyAll();
removeFollowerMock.VerifyAll();
#endregion
}
[TestMethod]
public async Task ProcessAsync_MultiInstances_Inbox_OneTweet_Error_MaxThreshold_Test()
{
#region Stubs
var tweetId = 1;
var host1 = "domain1.ext";
var host2 = "domain2.ext";
var inbox = "/user/inbox";
var userId1 = 2;
var userId2 = 3;
var userAcct = "user";
var userWithTweets = new UserWithDataToSync()
{
Tweets = new[]
{
new ExtractedTweet
{
Id = tweetId
}
},
User = new SyncTwitterUser
{
Acct = userAcct
},
Followers = new[]
{
new Follower
{
Id = userId1,
Host = host1,
InboxRoute = inbox
},
new Follower
{
Id = userId2,
Host = host2,
InboxRoute = inbox,
PostingErrorCount = 2147483600
},
}
};
#endregion
#region Mocks
var sendTweetsToInboxTaskMock = new Mock<ISendTweetsToInboxTask>(MockBehavior.Strict);
sendTweetsToInboxTaskMock
.Setup(x => x.ExecuteAsync(
It.Is<ExtractedTweet[]>(y => y.Length == 1),
It.Is<Follower>(y => y.Id == userId1),
It.Is<SyncTwitterUser>(y => y.Acct == userAcct)))
.Returns(Task.CompletedTask);
sendTweetsToInboxTaskMock
.Setup(x => x.ExecuteAsync(
It.Is<ExtractedTweet[]>(y => y.Length == 1),
It.Is<Follower>(y => y.Id == userId2),
It.Is<SyncTwitterUser>(y => y.Acct == userAcct)))
.Throws(new Exception());
var sendTweetsToSharedInboxTaskMock = new Mock<ISendTweetsToSharedInboxTask>(MockBehavior.Strict);
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
var settings = new InstanceSettings
{
FailingFollowerCleanUpThreshold = 0
};
var removeFollowerMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
removeFollowerMock
.Setup(x => x.ProcessAsync(It.Is<Follower>(y => y.Id == userId2)))
.Returns(Task.CompletedTask);
#endregion
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object, settings, removeFollowerMock.Object);
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
#region Validations
sendTweetsToInboxTaskMock.VerifyAll();
sendTweetsToSharedInboxTaskMock.VerifyAll();
followersDalMock.VerifyAll();
removeFollowerMock.VerifyAll();
#endregion
}
@ -704,15 +922,20 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
.Returns(Task.CompletedTask);
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
var settings = new InstanceSettings();
var removeFollowerMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
#endregion
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object);
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object, settings, removeFollowerMock.Object);
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
#region Validations
sendTweetsToInboxTaskMock.VerifyAll();
sendTweetsToSharedInboxTaskMock.VerifyAll();
followersDalMock.VerifyAll();
removeFollowerMock.VerifyAll();
#endregion
}
@ -790,15 +1013,20 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
.Returns(Task.CompletedTask);
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
var settings = new InstanceSettings();
var removeFollowerMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
#endregion
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object);
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object, settings, removeFollowerMock.Object);
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
#region Validations
sendTweetsToInboxTaskMock.VerifyAll();
sendTweetsToSharedInboxTaskMock.VerifyAll();
followersDalMock.VerifyAll();
removeFollowerMock.VerifyAll();
#endregion
}
}