From b762abec978f0dd8a402e7f9fd6e9b3a95556897 Mon Sep 17 00:00:00 2001
From: "Mr. Stallion" <mrstallion@nobody.nowhere.fauxdomain.ext>
Date: Sun, 27 Oct 2019 13:58:41 -0500
Subject: [PATCH] Fix for Imgur NSFW wall

---
 chat/ImagePreview.vue           |  35 ++++++++--
 chat/image-url-mutator.ts       | 116 ++++++++++++++++++++++++++++++++
 electron/Index.vue              |  19 ++++++
 learn/matcher.ts                |  86 +++++++++++++++++++++--
 readme.md                       |   6 +-
 site/character_page/infotag.vue |   2 +-
 6 files changed, 251 insertions(+), 13 deletions(-)
 create mode 100644 chat/image-url-mutator.ts

diff --git a/chat/ImagePreview.vue b/chat/ImagePreview.vue
index 0c3fd3c..85efc21 100644
--- a/chat/ImagePreview.vue
+++ b/chat/ImagePreview.vue
@@ -49,6 +49,10 @@
     [url=https://www.sex.com/pin/38152484-she-likes-it-rough/]Test[/url]
 
     [url=https://www.sex.com/pin/57537179-cock-slapping-hungry-tongue/]Test[/url]
+
+    [url=https://imgur.com/gallery/ILsb94I]Imgur gallery[/url]
+
+    [url=https://imgur.com/CIKv6sA]Imgur image[/url]
     */
 
     import * as _ from 'lodash';
@@ -57,6 +61,7 @@
     import { EventBus, EventBusEvent } from './event-bus';
     import {domain} from '../bbcode/core';
     import {ImagePreviewMutator} from './image-preview-mutator';
+    import {ImageUrlMutator} from './image-url-mutator';
     import {Point, screen, WebviewTag} from 'electron';
     import Timer = NodeJS.Timer;
 
@@ -76,13 +81,15 @@
         protected visible = false;
         protected url: string | null = 'about:blank';
         protected parent: ImagePreview;
+        protected debug: boolean;
 
-        abstract show(url: string): void;
+        abstract show(url: string): Promise<void>;
         abstract hide(): void;
         abstract match(domainName: string): boolean;
 
         constructor(parent: ImagePreview) {
             this.parent = parent;
+            this.debug = parent.debug;
         }
 
         isVisible(): boolean {
@@ -92,6 +99,10 @@
         getUrl(): string | null {
             return this.url;
         }
+
+        setDebug(debug: boolean): void {
+            this.debug = debug;
+        }
     }
 
 
@@ -102,7 +113,7 @@
         }
 
 
-        show(url: string): void {
+        async show(url: string): Promise<void> {
             this.visible = true;
             this.url = url;
         }
@@ -119,6 +130,9 @@
 
         protected allowCachedUrl = true;
 
+        protected urlMutator = new ImageUrlMutator(this.parent.debug);
+
+
         hide(): void {
             const wasVisible = this.visible;
 
@@ -139,20 +153,27 @@
         }
 
 
-        show(url: string): void {
+        setDebug(debug: boolean): void {
+            this.debug = debug;
+
+            this.urlMutator.setDebug(debug);
+        }
+
+
+        async show(url: string): Promise<void> {
             const webview = this.parent.getWebview();
 
             try {
                 if ((this.allowCachedUrl) && ((webview.getURL() === url) || (url === this.lastExternalUrl))) {
-                    if (this.parent.debug)
+                    if (this.debug)
                         console.log('ImagePreview: exec re-show mutator');
 
                     webview.executeJavaScript(this.parent.jsMutator.getReShowMutator());
                 } else {
-                    if (this.parent.debug)
+                    if (this.debug)
                         console.log('ImagePreview: must load; skip re-show because urls don\'t match', this.url, webview.getURL());
 
-                    webview.loadURL(url);
+                    webview.loadURL(await this.urlMutator.resolve(url));
                 }
 
             } catch (err) {
@@ -520,6 +541,8 @@
             this.debug = !this.debug;
 
             this.jsMutator.setDebug(this.debug);
+            this.localPreviewHelper.setDebug(this.debug);
+            this.externalPreviewHelper.setDebug(this.debug);
 
             if (this.debug) {
                 const webview = this.getWebview();
diff --git a/chat/image-url-mutator.ts b/chat/image-url-mutator.ts
new file mode 100644
index 0000000..2450fc7
--- /dev/null
+++ b/chat/image-url-mutator.ts
@@ -0,0 +1,116 @@
+import * as _ from 'lodash';
+import Axios from 'axios';
+
+export type UrlSolverCallback = (url: string, match: RegExpMatchArray) => Promise<string>;
+
+export interface UrlSolver {
+    matcher: RegExp;
+    solver: UrlSolverCallback;
+}
+
+
+export class ImageUrlMutator {
+
+    private solvers: UrlSolver[] = [];
+
+    private static IMGUR_CLIENT_ID = 'd60e27140a73b2e';
+
+    private debug: boolean;
+
+
+    constructor(debug: boolean) {
+        this.debug = debug;
+
+        this.init();
+    }
+
+    protected init(): void {
+        this.add(
+            /^https?:\/\/imgur.com\/gallery\/([a-zA-Z0-9]+)/,
+            async(url: string, match: RegExpMatchArray): Promise<string> => {
+                // Imgur Gallery
+                const galleryId = match[1];
+
+                try {
+                    const result = await Axios.get(
+                        `https://api.imgur.com/3/gallery/${galleryId}/images`,
+                        {
+                            headers: {
+                                Authorization: `Client-ID ${ImageUrlMutator.IMGUR_CLIENT_ID}`
+                            }
+                        }
+                    );
+
+                    const imageUrl = _.get(result, 'data.data.0.link', null);
+
+                    if (!imageUrl) {
+                        return url;
+                    }
+
+                    const imageCount = _.get(result, 'data.data.length', 1);
+
+                    if (this.debug)
+                        console.log('Imgur gallery', url, imageUrl, imageCount);
+
+                    return `${imageUrl}?flist_gallery_image_count=${imageCount}`;
+
+                } catch (err) {
+                    console.error('Imgur Gallery Failure', url, err);
+                    return url;
+                }
+            }
+        );
+
+        // must be AFTER gallery test
+        this.add(
+            /^https?:\/\/imgur.com\/([a-zA-Z0-9]+)/,
+            async(url: string, match: RegExpMatchArray): Promise<string> => {
+                // Single Imgur Image
+                const imageId = match[1];
+
+                try {
+                    const result = await Axios.get(
+                        `https://api.imgur.com/3/image/${imageId}`,
+                        {
+                            headers: {
+                                Authorization: `Client-ID ${ImageUrlMutator.IMGUR_CLIENT_ID}`
+                            }
+                        }
+                    );
+
+                    const imageUrl = _.get(result, 'data.data.link', url);
+
+                    if (this.debug)
+                        console.log('Imgur image', url, imageUrl);
+
+                    return imageUrl as string;
+                } catch (err) {
+                    console.error('Imgur Image Failure', url, err);
+                    return url;
+                }
+            }
+        );
+    }
+
+
+    setDebug(debug: boolean): void {
+        this.debug = debug;
+    }
+
+
+    protected add(matcher: RegExp, solver: UrlSolverCallback): void {
+        this.solvers.push({matcher, solver});
+    }
+
+
+    async resolve(url: string): Promise<string> {
+        const match = _.find(this.solvers, (s: UrlSolver) => url.match(s.matcher)) as (UrlSolver | undefined);
+
+        if (!match) {
+            return url;
+        }
+
+        return match.solver(url, url.match(match.matcher) as RegExpMatchArray);
+    }
+}
+
diff --git a/electron/Index.vue b/electron/Index.vue
index 509fc99..5763ded 100644
--- a/electron/Index.vue
+++ b/electron/Index.vue
@@ -99,6 +99,25 @@
     const webContents = electron.remote.getCurrentWebContents();
     const parent = electron.remote.getCurrentWindow().webContents;
 
+    // Allow requests to imgur.com
+    const session = electron.remote.session;
+
+    /* tslint:disable:no-unsafe-any no-any no-unnecessary-type-assertion */
+    session!.defaultSession!.webRequest!.onBeforeSendHeaders(
+        {
+            urls: [
+                'https?://(api|i).imgur.com/*'
+            ]
+        },
+        (details: any, callback: any) => {
+            details.requestHeaders['Origin'] = null;
+            details.headers['Origin'] = null;
+
+            callback({requestHeaders: details.requestHeaders});
+        }
+    );
+
+
     log.info('About to load keytar');
 
     /* tslint:disable: no-any no-unsafe-any */ //because this is hacky
diff --git a/learn/matcher.ts b/learn/matcher.ts
index 89390e0..a56aacf 100644
--- a/learn/matcher.ts
+++ b/learn/matcher.ts
@@ -9,7 +9,7 @@ export enum TagId {
     Gender = 3,
     Build = 13,
     FurryPreference = 29,
-    BdsmRole = 15,
+    SubDomRole = 15,
     Position = 41,
     BodyType = 51,
     ApparentAge = 64,
@@ -29,6 +29,14 @@ export enum Gender {
     Shemale = 141
 }
 
+export enum SubDomRole {
+    AlwaysSubmissive = 7,
+    UsuallySubmissive = 8,
+    Switch = 9,
+    UsuallyDominant = 10,
+    AlwaysDominant = 11
+}
+
 export enum Orientation {
     Straight = 4,
     Gay = 5,
@@ -73,6 +81,8 @@ enum Kink {
     Ageplay = 196,
     UnderageCharacters = 207,
 
+    RoleReversal = 408,
+
     AnthroCharacters = 587,
     Humans = 609,
 
@@ -189,7 +199,7 @@ const fchatGenderMap: FchatGenderMap = {
     Shemale: Gender.Shemale,
     Herm: Gender.Herm,
     'Male-Herm': Gender.MaleHerm,
-    'Cunt-boy': Gender.Cuntboy,
+    'Cunt-Boy': Gender.Cuntboy,
     Transgender: Gender.Transgender
 };
 
@@ -285,6 +295,7 @@ export class CharacterAnalysis {
     readonly species: Species | null;
     readonly furryPreference: FurryPreference | null;
     readonly age: number | null;
+    readonly subDomRole: SubDomRole | null;
 
     readonly isAnthro: boolean | null;
     readonly isHuman: boolean | null;
@@ -297,6 +308,7 @@ export class CharacterAnalysis {
         this.orientation = Matcher.getTagValueList(TagId.Orientation, c);
         this.species = Matcher.species(c);
         this.furryPreference = Matcher.getTagValueList(TagId.FurryPreference, c);
+        this.subDomRole = Matcher.getTagValueList(TagId.SubDomRole, c);
 
         const ageTag = Matcher.getTagValue(TagId.Age, c);
 
@@ -360,7 +372,8 @@ export class Matcher {
                 [TagId.Gender]: this.resolveGenderScore(),
                 [TagId.Age]: this.resolveAgeScore(),
                 [TagId.FurryPreference]: this.resolveFurryPairingsScore(),
-                [TagId.Species]: this.resolveSpeciesScore()
+                [TagId.Species]: this.resolveSpeciesScore(),
+                [TagId.SubDomRole]: this.resolveSubDomScore()
             },
 
             info: {
@@ -610,7 +623,6 @@ export class Matcher {
 
     private resolveGenderScore(): Score {
         const you = this.you;
-
         const theirGender = this.theirAnalysis.gender;
 
         if (theirGender === null)
@@ -625,6 +637,72 @@ export class Matcher {
         return new Score(Scoring.NEUTRAL);
     }
 
+
+    private resolveSubDomScore(): Score {
+        const you = this.you;
+        const yourSubDomRole = this.yourAnalysis.subDomRole;
+        const theirSubDomRole = this.theirAnalysis.subDomRole;
+        const yourRoleReversalPreference = Matcher.getKinkPreference(you, Kink.RoleReversal);
+
+        if ((!yourSubDomRole) || (!theirSubDomRole))
+            return new Score(Scoring.NEUTRAL);
+
+        if ((yourSubDomRole === SubDomRole.AlwaysDominant) || (yourSubDomRole === SubDomRole.UsuallyDominant)) {
+            if (theirSubDomRole === SubDomRole.Switch)
+                return new Score(Scoring.WEAK_MATCH, `Likes <span>switches</span>`);
+
+            if ((theirSubDomRole === SubDomRole.AlwaysSubmissive) || (theirSubDomRole === SubDomRole.UsuallySubmissive))
+                return new Score(Scoring.MATCH, `Loves <span>submissives</span>`);
+
+            if (yourRoleReversalPreference === KinkPreference.Favorite)
+                return new Score(Scoring.MATCH, `Loves <span>role reversal</span>`);
+
+            if (yourRoleReversalPreference === KinkPreference.Yes)
+                return new Score(Scoring.MATCH, `Likes <span>role reversal</span>`);
+
+            if ((yourSubDomRole === SubDomRole.AlwaysDominant) && (theirSubDomRole === SubDomRole.AlwaysDominant))
+                return new Score(Scoring.MISMATCH, 'No <span>dominants</span>');
+
+            return new Score(Scoring.WEAK_MISMATCH, 'Hesitant on <span>dominants</span>');
+        } else if ((yourSubDomRole === SubDomRole.AlwaysSubmissive) || (yourSubDomRole === SubDomRole.UsuallySubmissive)) {
+            if (theirSubDomRole === SubDomRole.Switch)
+                return new Score(Scoring.WEAK_MATCH, `Likes <span>switches</span>`);
+
+            if ((theirSubDomRole === SubDomRole.AlwaysDominant) || (theirSubDomRole === SubDomRole.UsuallyDominant))
+                return new Score(Scoring.MATCH, `Loves <span>dominants</span>`);
+
+            if (yourRoleReversalPreference === KinkPreference.Favorite)
+                return new Score(Scoring.MATCH, `Loves <span>role reversal</span>`);
+
+            if (yourRoleReversalPreference === KinkPreference.Yes)
+                return new Score(Scoring.MATCH, `Likes <span>role reversal</span>`);
+
+            if ((yourSubDomRole === SubDomRole.AlwaysSubmissive) && (theirSubDomRole === SubDomRole.AlwaysSubmissive))
+                return new Score(Scoring.MISMATCH, 'No <span>submissives</span>');
+
+            return new Score(Scoring.WEAK_MISMATCH, 'Hesitant on <span>submissives</span>');
+        }
+
+        // You must be a switch
+        if (theirSubDomRole === SubDomRole.Switch)
+            return new Score(Scoring.MATCH, `Loves <span>switches</span>`);
+
+        // if (yourRoleReversalPreference === KinkPreference.Favorite)
+        //     return new Score(Scoring.MATCH, `Loves <span>role reversal</span>`);
+        //
+        // if (yourRoleReversalPreference === KinkPreference.Yes)
+        //     return new Score(Scoring.MATCH, `Likes <span>role reversal</span>`);
+
+        if ((theirSubDomRole === SubDomRole.AlwaysDominant) || (theirSubDomRole === SubDomRole.UsuallyDominant))
+            return new Score(Scoring.MATCH, `Loves <span>dominants</span>`);
+
+        if ((theirSubDomRole === SubDomRole.AlwaysSubmissive) || (theirSubDomRole === SubDomRole.UsuallySubmissive))
+            return new Score(Scoring.MATCH, `Loves <span>submissives</span>`);
+
+        return new Score(Scoring.NEUTRAL);
+    }
+
+
     static getTagValue(tagId: number, c: Character): CharacterInfotag | undefined {
         return c.infotags[tagId];
     }
diff --git a/readme.md b/readme.md
index bc4b453..afcee9d 100644
--- a/readme.md
+++ b/readme.md
@@ -29,7 +29,7 @@ This repository contains a heavily customized version of the mainline F-Chat 3.0
     *    Kinks are auto-compared when viewing character profile
     *    Custom kink explanations can be expanded inline
     *    Custom kinks are highlighted
-    *    Gender, anthro/human preference, age, and sexual preference are highlighted if compatible or incompatible
+    *    Gender, anthro/human preference, age, sexual preference, and sub/dom preference are highlighted if compatible or incompatible
     *    Guestbook, friend, and group counts are visible on tabs
     *    Character images are expanded inline
     *    Cleaner presentation for the side bar details (age, etc.), sorted in most relevant order
@@ -43,7 +43,7 @@ This repository contains a heavily customized version of the mainline F-Chat 3.0
     *    Search filters can be reset
 *   General
     *    Character profiles, guestbooks, friend lists, and image lists are cached for faster access.
-    *    Open conversation dialog for typing in a character name
+    *    Conversation dialog can be opened by typing in a character name
 
 
 ## How to Set Up Ads
@@ -76,6 +76,8 @@ This repository contains a heavily customized version of the mainline F-Chat 3.0
 *   Configuration dialog for the player to change match weighs, preview settings, etc.
 *   Conversation bot API
 *   Character comparison should consider D/s preference
+*   Fix collapsible details on character view
+*   Filter unmatching ads is not channel specific -- it's either on everywhere or nowhere
 
 
 # F-List Exported
diff --git a/site/character_page/infotag.vue b/site/character_page/infotag.vue
index 640c10c..a4f5a1e 100644
--- a/site/character_page/infotag.vue
+++ b/site/character_page/infotag.vue
@@ -51,7 +51,7 @@
 
 
         theirInterestIsRelevant(id: number): boolean {
-            return ((id === TagId.FurryPreference) || (id === TagId.Orientation));
+            return ((id === TagId.FurryPreference) || (id === TagId.Orientation) || (id === TagId.SubDomRole));
         }