Skip to content

Commit

Permalink
Feat: Add more complex filters for synchronization (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
RikudouSage authored Sep 12, 2023
1 parent 8b99930 commit becd2ff
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,6 @@ <h3 class="card-title">Synchronization</h3>
<input type="text" id="inputTotp" formControlName="totp" class="form-control" aria-describedby="inputTotpDescription" />
<small id="inputTotpDescription">Only if your account is 2FA protected.</small>
</div>
<div class="form-group">
<div class="custom-control custom-switch custom-switch-on-danger">
<input class="custom-control-input" type="checkbox" id="inputPurge" formControlName="purgeBlacklist" aria-describedby="inputPurgeDescription" />
<label for="inputPurge" class="custom-control-label">Purge blacklist</label>
</div>
<small id="inputPurgeDescription">If enabled, your blacklist will be purged before synchronizing the new one. Make sure you've backed up the original one.</small>
</div>
<div class="form-group">
<label for="inputMode">Mode</label>
<select formControlName="mode" id="inputMode" aria-describedby="inputModeDescription" class="form-control">
Expand All @@ -68,13 +61,36 @@ <h3 class="card-title">Synchronization</h3>
<ng-template #whitelistedInstancesSelect>
<label for="inputCustomInstances">Custom instances</label>
<select formControlName="customInstances" id="inputCustomInstances" *ngIf="whitelistedInstancesList" tom-select multiple [maxItems]="null">
<option *ngFor="let option of whitelistedInstancesList" [value]="option">{{option}}</option>
<option *ngFor="let option of whitelistedInstancesList" [value]="option.domain">{{option.domain}}</option>
</select>
</ng-template>
</div>
<div class="form-group">
<div class="custom-control custom-switch custom-switch-on-danger">
<input class="custom-control-input" type="checkbox" id="inputPurge" formControlName="purgeBlacklist" aria-describedby="inputPurgeDescription" />
<label for="inputPurge" class="custom-control-label">Purge blacklist</label>
</div>
<small id="inputPurgeDescription">If enabled, your blacklist will be purged before synchronizing the new one. Make sure you've backed up the original one.</small>
</div>
<div class="form-group">
<div class="custom-control custom-switch custom-switch-on-danger">
<input class="custom-control-input" type="checkbox" id="inputFilterReasons" formControlName="filterByReasons" aria-describedby="inputFilterReasonsDescription" />
<label for="inputFilterReasons" class="custom-control-label">Filter by reasons</label>
</div>
<small id="inputFilterReasonsDescription">
If enabled, only instances with censure reasons you specify will get blacklisted. Note that your own blacklist is always synchronized in full.
</small>
</div>
<div class="form-group position-relative" *ngIf="form.controls.filterByReasons.value">
<app-loader *ngIf="loadingReasons else reasonsSelect" />
<ng-template #reasonsSelect>
<select formControlName="reasonsFilter" tom-select multiple [create]="true" [maxItems]="null">
<option *ngFor="let option of availableReasons" [value]="option">{{option}}</option>
</select>
</ng-template>
</div>
<button type="submit" [disabled]="!form.valid" class="btn btn-primary">Synchronize</button>
</form>

<div class="row mt-4">
<div class="col-md-12 minimum-height">
<h3>Preview</h3>
Expand All @@ -87,7 +103,7 @@ <h3>Preview</h3>
<table class="table table-bordered mt-3">
<tr *ngFor="let instance of added" class="bg-success">
<td>
{{instance}}
{{instance.domain}}
<app-tooltip text="This instance is only on Fediseer and not synchronized to your instance." />
</td>
</tr>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {FediseerApiService} from "../../../services/fediseer-api.service";
import {ApiResponseHelperService} from "../../../services/api-response-helper.service";
import {MessageService} from "../../../services/message.service";
import {HttpErrorResponse} from "@angular/common/http";
import {InstanceDetailResponse} from "../../../response/instance-detail.response";
import {NormalizedInstanceDetailResponse} from "../../../response/normalized-instance-detail.response";

@Component({
selector: 'app-synchronize',
Expand All @@ -21,7 +23,7 @@ export class SynchronizeLemmyComponent implements OnInit {
protected readonly SynchronizationMode = SynchronizationMode;
protected readonly Number = Number;

private cache: {[key: string]: string[] | null} = {};
private cache: {[key: string]: InstanceDetailResponse[] | null} = {};

public originallyBlockedInstances: string[] = [];

Expand All @@ -33,14 +35,19 @@ export class SynchronizeLemmyComponent implements OnInit {
purgeBlacklist: new FormControl<boolean>(false, [Validators.required]),
mode: new FormControl<SynchronizationMode>(SynchronizationMode.Own, [Validators.required]),
customInstances: new FormControl<string[]>([]),
filterByReasons: new FormControl<boolean>(false),
reasonsFilter: new FormControl<string[]>([]),
});

public loading = true;
public loadingPreview = false;
public loadingWhitelistedInstances = false;
public loadingReasons = false;

public added: string[] = [];
public added: InstanceDetailResponse[] = [];
public removed: string[] = [];
public whitelistedInstancesList: string[] | null = null;
public whitelistedInstancesList: InstanceDetailResponse[] | null = null;
public availableReasons: string[] | null = null;

constructor(
private readonly database: DatabaseService,
Expand All @@ -64,6 +71,8 @@ export class SynchronizeLemmyComponent implements OnInit {
instance: this.authManager.currentInstanceSnapshot.name,
password: this.database.lemmyPassword ?? '',
customInstances: settings.customInstances,
reasonsFilter: settings.reasonsFilter,
filterByReasons: settings.filterByReasons,
});

const instances = await this.getBlockedInstancesFromSource(this.authManager.currentInstanceSnapshot.name);
Expand All @@ -83,6 +92,8 @@ export class SynchronizeLemmyComponent implements OnInit {
purge: values.purgeBlacklist ?? false,
mode: values.mode ?? SynchronizationMode.Own,
customInstances: values.customInstances ?? [],
filterByReasons: values.filterByReasons ?? false,
reasonsFilter: values.reasonsFilter ?? [],
});
});
this.form.controls.mode.valueChanges.subscribe(mode => {
Expand All @@ -100,10 +111,40 @@ export class SynchronizeLemmyComponent implements OnInit {

this.loadDiffs(mode);
});
this.form.controls.filterByReasons.valueChanges.subscribe(filter => {
const mode = this.form.controls.mode.value;
if (filter === null || mode === null) {
return;
}
this.loadReasons();
this.loadDiffs(mode);
});
this.form.controls.reasonsFilter.valueChanges.subscribe(reasons => {
const mode = this.form.controls.mode.value;
if (reasons === null || mode === null) {
return;
}
this.loadDiffs(mode);
});
if (this.form.controls.mode.value) {
this.loadDiffs(this.form.controls.mode.value);
this.loadCustomInstancesSelect(this.form.controls.mode.value);
}
if (this.form.controls.filterByReasons.value) {
this.loadReasons();
}
}

private async loadReasons() {
if (this.availableReasons === null) {
this.loadingReasons = true;
const reasons = await toPromise(this.fediseerApi.getUsedReasons());
if (reasons === null) {
this.messageService.createError('Failed getting list of reasons from the server');
}
this.availableReasons = reasons;
this.loadingReasons = false;
}
}

private async loadCustomInstancesSelect(mode: SynchronizationMode) {
Expand All @@ -123,12 +164,11 @@ export class SynchronizeLemmyComponent implements OnInit {
const guaranteed = responses[2].successResponse!.instances.map(instance => instance.domain);

this.whitelistedInstancesList = responses[0].successResponse!.instances
.map(instance => instance.domain)
.sort((a, b) => {
const endorsedA = endorsed.includes(a);
const endorsedB = endorsed.includes(b);
const guaranteedA = guaranteed.includes(a);
const guaranteedB = guaranteed.includes(b);
const endorsedA = endorsed.includes(a.domain);
const endorsedB = endorsed.includes(b.domain);
const guaranteedA = guaranteed.includes(a.domain);
const guaranteedB = guaranteed.includes(b.domain);

if (!endorsedA && !endorsedB && !guaranteedA && !guaranteedB) {
return 0;
Expand Down Expand Up @@ -163,9 +203,10 @@ export class SynchronizeLemmyComponent implements OnInit {
this.loadingPreview = false;
return;
}
const instancesToBanString = instancesToBan.map(instance => instance.domain);

this.added = instancesToBan.filter(item => !this.originallyBlockedInstances.includes(item));
this.removed = this.originallyBlockedInstances.filter(item => !instancesToBan.includes(item));
this.added = instancesToBan.filter(item => !this.originallyBlockedInstances.includes(item.domain));
this.removed = this.originallyBlockedInstances.filter(item => !instancesToBanString.includes(item));
this.loadingPreview = false;
}

Expand Down Expand Up @@ -213,7 +254,7 @@ export class SynchronizeLemmyComponent implements OnInit {
}

private async getEndorsedCensureChain(instance: string): Promise<string[]> {
const result = await toPromise(this.fediseerApi.getEndorsementsByInstance([instance]).pipe(
return await toPromise(this.fediseerApi.getEndorsementsByInstance([instance]).pipe(
map(response => {
if (this.apiResponseHelper.handleErrors([response])) {
return [];
Expand All @@ -222,19 +263,15 @@ export class SynchronizeLemmyComponent implements OnInit {
return response.successResponse!.instances.map(instance => instance.domain);
}),
));

result.push(instance);

return result;
}

private async getCensuresByInstances(instances: string[]): Promise<string[] | null> {
private async getCensuresByInstances(instances: string[]): Promise<InstanceDetailResponse[] | null> {
const instancesResponse = await toPromise(this.fediseerApi.getCensuresByInstances(instances));
if (this.apiResponseHelper.handleErrors([instancesResponse])) {
return null;
}

return instancesResponse.successResponse!.instances.map(instance => instance.domain);
return instancesResponse.successResponse!.instances;
}

private async getBlockedInstancesFromSource(instance: string): Promise<string[] | null> {
Expand Down Expand Up @@ -272,7 +309,10 @@ export class SynchronizeLemmyComponent implements OnInit {
return;
}

const newInstances = this.form.controls.purgeBlacklist.value! ? instancesToBan : [...new Set([...originalInstances, ...instancesToBan])];
const newInstances =
this.form.controls.purgeBlacklist.value!
? instancesToBan.map(instance => instance.domain)
: [...new Set([...originalInstances, ...instancesToBan.map(instance => instance.domain)])];

try {
await toPromise(this.lemmyApi.updateBlacklist(myInstance, jwt, newInstances));
Expand All @@ -288,36 +328,46 @@ export class SynchronizeLemmyComponent implements OnInit {
}
}

private async getInstancesToBan(mode: SynchronizationMode): Promise<string[] | null> {
private async getInstancesToBan(mode: SynchronizationMode): Promise<InstanceDetailResponse[] | null> {
const myInstance = this.authManager.currentInstanceSnapshot.name;
let cacheKey: string = mode;

if (mode === SynchronizationMode.CustomInstances && this.form.controls.customInstances.value) {
cacheKey += this.form.controls.customInstances.value.join('|') ?? '';
cacheKey += this.form.controls.customInstances.value.join('|');
}
if (this.form.controls.filterByReasons.value && this.form.controls.reasonsFilter.value) {
cacheKey += this.form.controls.reasonsFilter.value!.join('|');
}

this.cache[myInstance] ??= await this.getCensuresByInstances([myInstance]);
this.cache[cacheKey] ??= await (async () => {
let sourceFrom: string[];
switch (mode) {
case SynchronizationMode.Own:
sourceFrom = [myInstance];
sourceFrom = [];
break;
case SynchronizationMode.Endorsed:
sourceFrom = await this.getEndorsedCensureChain(myInstance);
break;
case SynchronizationMode.CustomInstances:
sourceFrom = [myInstance, ...(this.form.controls.customInstances.value ?? [])];
sourceFrom = this.form.controls.customInstances.value ?? [];
break;
default:
throw new Error(`Unsupported mode: ${mode}`);
}

if (!sourceFrom.length) {
this.messageService.createError('No instances to get censures from.');
return [];
let foreignInstanceBlacklist = await this.getCensuresByInstances(sourceFrom) ?? [];
if (this.form.controls.filterByReasons.value && this.form.controls.reasonsFilter.value) {
const reasons = this.form.controls.reasonsFilter.value!;
foreignInstanceBlacklist = foreignInstanceBlacklist.filter(
instance => NormalizedInstanceDetailResponse.fromInstanceDetail(instance).unmergedCensureReasons.filter(
reason => reasons.includes(reason),
).length,
);
}

return await this.getCensuresByInstances(sourceFrom);

return [...this.cache[myInstance]!, ...foreignInstanceBlacklist];
})();

return this.cache[cacheKey]!;
Expand Down
2 changes: 2 additions & 0 deletions src/app/services/database.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ export class DatabaseService {
purge: false,
mode: SynchronizationMode.Own,
customInstances: [],
filterByReasons: false,
reasonsFilter: [],
};
}

Expand Down
2 changes: 2 additions & 0 deletions src/app/types/synchronize-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ export interface SynchronizeSettings {
mode: SynchronizationMode;
purge: boolean;
customInstances: string[];
filterByReasons: boolean;
reasonsFilter: string[];
}

0 comments on commit becd2ff

Please sign in to comment.