Smart filters and M1 build

This commit is contained in:
Mr. Stallion 2022-01-02 16:37:57 -06:00
parent 65ab5ffa32
commit bbc2ca2f83
16 changed files with 415 additions and 136 deletions

View File

@ -1,7 +1,8 @@
# Changelog
## Canary
* Added a way to hide/filter out characters, messages, and ads (Settings > Identity Politics)
## 1.17.0
* Added a way to hide/filter out characters, messages, and ads (Settings > Smart Filters)
* Added MacOS M1 build
## 1.16.2
* Fixed broken auto-ads

View File

@ -1,7 +1,7 @@
# Download
[Windows](https://github.com/mrstallion/fchat-rising/releases/download/v1.16.2/F-Chat-Rising-1.16.2-win.exe) (75 MB)
| [MacOS](https://github.com/mrstallion/fchat-rising/releases/download/v1.16.2/F-Chat-Rising-1.16.2-macos.dmg) (76 MB)
| [Linux](https://github.com/mrstallion/fchat-rising/releases/download/v1.16.2/F-Chat-Rising-1.16.2-linux.AppImage) (76 MB)
[Windows](https://github.com/mrstallion/fchat-rising/releases/download/v1.17.0/F-Chat-Rising-1.17.0-win.exe) (75 MB)
| [MacOS](https://github.com/mrstallion/fchat-rising/releases/download/v1.17.0/F-Chat-Rising-1.17.0-macos.dmg) (76 MB)
| [Linux](https://github.com/mrstallion/fchat-rising/releases/download/v1.17.0/F-Chat-Rising-1.17.0-linux.AppImage) (76 MB)
# F-Chat Rising

View File

@ -270,7 +270,7 @@
core.connection.onMessage('FKS', async (data) => {
const results = data.characters.map((x) => ({ character: core.characters.get(x), profile: null }))
.filter((x) => core.state.hiddenUsers.indexOf(x.character.name) === -1 && !x.character.isIgnored)
.filter((x) => this.isSpeciesMatch(x) && this.isBodyTypeMatch(x) && this.isSmartFilterMatch(x))
.filter((x) => this.isSpeciesMatch(x) && this.isBodyTypeMatch(x) && !this.isSmartFiltered(x))
.sort(sort);
// pre-warm cache
@ -348,7 +348,7 @@
private resort(results = this.results) {
this.results = (_.filter(
results,
(x) => this.isSpeciesMatch(x) && this.isBodyTypeMatch(x)
(x) => this.isSpeciesMatch(x) && this.isBodyTypeMatch(x) && !this.isSmartFiltered(x)
) as SearchResult[]).sort(sort);
}
@ -399,12 +399,12 @@
return this.data.bodytypes.indexOf(bodytype!.value) > -1
}
isSmartFilterMatch(result: SearchResult) {
isSmartFiltered(result: SearchResult) {
if (!core.state.settings.risingFilter.hideSearchResults) {
return true;
return false;
}
return result.profile ? !result.profile?.match.isFiltered : true;
return !!result.profile?.match.isFiltered;
}
getSpeciesOptions(): SearchSpecies[] {

View File

@ -1,7 +1,7 @@
<template>
<modal :action="l('settings.action')" @submit="submit" @open="load()" id="settings" dialogClass="w-100">
<tabs style="flex-shrink:0;margin-bottom:10px" v-model="selectedTab"
:tabs="[l('settings.tabs.general'), l('settings.tabs.notifications'), 'F-Chat Rising 🦄', 'Identity Politics 🦄', l('settings.tabs.hideAds'), l('settings.tabs.import')]"></tabs>
:tabs="[l('settings.tabs.general'), l('settings.tabs.notifications'), 'F-Chat Rising 🦄', 'Smart Filters 🦄', l('settings.tabs.hideAds'), l('settings.tabs.import')]"></tabs>
<div v-show="selectedTab === '0'">
<div class="form-group">
<label class="control-label" for="disallowedTags">{{l('settings.disallowedTags')}}</label>
@ -202,6 +202,14 @@
</div>
<div v-show="selectedTab === '3'">
<div class="warning">
<h5>Danger Zone!</h5>
<div>By activating filtering, you may no longer be able to see or receive all messages from F-Chat.
Filters do not apply to friends or bookmarked characters.</div>
<div>Beta version. Some of these features and behaviors may be removed or significantly changed in the future.</div>
</div>
<h5>Visibility</h5>
<div class="form-group filters">
@ -235,9 +243,9 @@
Hide <b>private messages</b> (PMs) from matching characters
</label>
<label class="control-label" for="risingFilter.penalizeMatches">
<input type="checkbox" id="risingFilter.penalizeMatches" v-model="risingFilter.penalizeMatches"/>
Penalize <b>match scores</b> for matching characters
<label class="control-label" for="risingFilter.showFilterIcon">
<input type="checkbox" id="risingFilter.showFilterIcon" v-model="risingFilter.showFilterIcon"/>
Show <b>filter icon</b> on matching characters
</label>
</div>
@ -246,17 +254,27 @@
<input type="checkbox" id="risingFilter.autoReply" v-model="risingFilter.autoReply"/>
Send an automatic 'no thank you' response to matching characters if they message you
</label>
<label class="control-label" for="risingFilter.penalizeMatches">
<input type="checkbox" id="risingFilter.penalizeMatches" v-model="risingFilter.penalizeMatches"/>
Penalize <b>match scores</b> for matching characters
</label>
<label class="control-label" for="risingFilter.rewardNonMatches">
<input type="checkbox" id="risingFilter.rewardNonMatches" v-model="risingFilter.rewardNonMatches"/>
Increase <b>match scores</b> for non-matching characters
</label>
</div>
<h5>Character Age Match</h5>
<div class="form-group">Leave empty for no limit.</div>
<div class="form-group">
<label class="control-label" for="risingFilter.minAge">Characters younger than</label>
<input id="risingFilter.minAge" type="number" class="form-control" v-model="risingFilter.minAge"/>
<label class="control-label" for="risingFilter.minAge">Characters younger than (years)</label>
<input id="risingFilter.minAge" type="number" class="form-control" v-model="risingFilter.minAge" placeholder="Enter age" />
<label class="control-label" for="risingFilter.maxAge">Characters older than</label>
<input id="risingFilter.maxAge" type="number" class="form-control" v-model="risingFilter.maxAge"/>
<label class="control-label" for="risingFilter.maxAge">Characters older than (years)</label>
<input id="risingFilter.maxAge" type="number" class="form-control" v-model="risingFilter.maxAge" placeholder="Enter age" />
</div>
<h5>Type Match</h5>
@ -271,7 +289,7 @@
<div class="form-group">Filters are not applied to these character names. Separate names with a linefeed.</div>
<div class="form-group">
<textarea class="form-control" :value="getExceptionList()" @change="(v) => setExceptionList(v)"></textarea>
<textarea class="form-control" :value="getExceptionList()" @change="(v) => setExceptionList(v)" placeholder="Enter names"></textarea>
</div>
</div>
@ -510,7 +528,7 @@
}
</script>
<style>
<style lang="scss">
#settings .form-group {
margin-left: 0;
margin-right: 0;
@ -522,4 +540,23 @@
margin-left: 5px;
list-style: none;
}
#settings .warning {
border: 1px solid var(--warning);
padding: 10px;
margin-bottom: 20px;
border-radius: 3px;
div {
margin-top: 10px;
}
}
#settings .form-group.filters.age label {
padding-top: 10px;
}
#settings .form-group.filters.age input {
margin-left: 5px;
}
</style>

View File

@ -1,5 +1,5 @@
<!-- Linebreaks inside this template will break BBCode views -->
<template><span :class="userClass" v-bind:bbcodeTag.prop="'user'" v-bind:character.prop="character" v-bind:channel.prop="channel" @mouseover.prevent="show()" @mouseenter.prevent="show()" @mouseleave.prevent="dismiss()" @click.middle.prevent.stop="toggleStickyness()" @click.right.passive="dismiss(true)" @click.left.passive="dismiss(true)"><span v-if="!!statusClass" :class="statusClass"></span><span v-if="!!rankIcon" :class="rankIcon"></span>{{character.name}}<span v-if="!!matchClass" :class="matchClass">{{getMatchScoreTitle(matchScore)}}</span></span></template>
<template><span :class="userClass" v-bind:bbcodeTag.prop="'user'" v-bind:character.prop="character" v-bind:channel.prop="channel" @mouseover.prevent="show()" @mouseenter.prevent="show()" @mouseleave.prevent="dismiss()" @click.middle.prevent.stop="toggleStickyness()" @click.right.passive="dismiss(true)" @click.left.passive="dismiss(true)"><span v-if="!!statusClass" :class="statusClass"></span><span v-if="!!rankIcon" :class="rankIcon"></span><span v-if="!!smartFilterIcon" :class="smartFilterIcon"></span>{{character.name}}<span v-if="!!matchClass" :class="matchClass">{{getMatchScoreTitle(matchScore)}}</span></span></template>
<script lang="ts">
@ -36,6 +36,7 @@ export function getStatusIcon(status: Character.Status): string {
export interface StatusClasses {
rankIcon: string | null;
smartFilterIcon: string | null;
statusClass: string | null;
matchClass: string | null;
matchScore: number | string | null;
@ -54,6 +55,7 @@ export function getStatusClasses(
let statusClass = null;
let matchClass = null;
let matchScore = null;
let smartFilterIcon: string | null = null;
if(character.isChatOp) {
rankIcon = 'far fa-gem';
@ -68,23 +70,30 @@ export function getStatusClasses(
if ((showStatus) || (character.status === 'crown'))
statusClass = `fa-fw ${getStatusIcon(character.status)}`;
if ((core.state.settings.risingAdScore) && (showMatch)) {
const cache = core.cache.profileCache.getSync(character.name);
const cache = ((showMatch) && ((core.state.settings.risingAdScore) || (core.state.settings.risingFilter.showFilterIcon)))
? core.cache.profileCache.getSync(character.name)
: undefined;
if (cache) {
if ((cache.match.searchScore >= kinkMatchWeights.unicornThreshold) && (cache.match.matchScore === Scoring.MATCH)) {
matchClass = 'match-found unicorn';
matchScore = 'unicorn';
} else {
matchClass = `match-found ${Score.getClasses(cache.match.matchScore)}`;
matchScore = cache.match.matchScore;
}
// undefined == not interested
// null == no cache hit
if (cache === null) {
void core.cache.addProfile(character.name);
}
if ((core.state.settings.risingAdScore) && (showMatch) && (cache)) {
if ((cache.match.searchScore >= kinkMatchWeights.unicornThreshold) && (cache.match.matchScore === Scoring.MATCH)) {
matchClass = 'match-found unicorn';
matchScore = 'unicorn';
} else {
/* tslint:disable-next-line no-floating-promises */
core.cache.addProfile(character.name);
matchClass = `match-found ${Score.getClasses(cache.match.matchScore)}`;
matchScore = cache.match.matchScore;
}
}
if (core.state.settings.risingFilter.showFilterIcon && cache?.match.isFiltered) {
smartFilterIcon = 'user-filter fas fa-filter';
}
const gender = character.gender !== undefined ? character.gender.toLowerCase() : 'none';
const isBookmark = (showBookmark) && (core.connection.isOpen) && (core.state.settings.colorBookmarks) &&
@ -98,6 +107,7 @@ export function getStatusClasses(
matchClass,
matchScore,
userClass,
smartFilterIcon,
isBookmark
};
}
@ -130,6 +140,7 @@ export default class UserView extends Vue {
userClass = '';
rankIcon: string | null = null;
smartFilterIcon: string | null = null;
statusClass: string | null = null;
matchClass: string | null = null;
matchScore: number | string | null = null;
@ -198,6 +209,7 @@ export default class UserView extends Vue {
const res = getStatusClasses(this.character, this.channel, !!this.showStatus, !!this.bookmark, !!this.match);
this.rankIcon = res.rankIcon;
this.smartFilterIcon = res.smartFilterIcon;
this.statusClass = res.statusClass;
this.matchClass = res.matchClass;
this.matchScore = res.matchScore;

View File

@ -56,23 +56,28 @@ export class Settings implements ISettings {
risingColorblindMode = false;
risingFilter = {
hideAds: false,
hideSearchResults: false,
hideAds: true,
hideSearchResults: true,
hideChannelMembers: false,
hidePublicChannelMessages: false,
hidePrivateChannelMessages: false,
hidePrivateMessages: false,
penalizeMatches: false,
showFilterIcon: true,
penalizeMatches: true,
rewardNonMatches: false,
autoReply: true,
minAge: null,
maxAge: null,
smartFilters: {
ageplay: false,
anthro: false,
female: false,
feral: false,
human: false,
hyper: false,
incest: false,
intersex: false,
male: false,
microMacro: false,
obesity: false,
pokemon: false,

View File

@ -13,7 +13,6 @@ import throat from 'throat';
import Bluebird from 'bluebird';
import log from 'electron-log';
import isChannel = Interfaces.isChannel;
import isPrivate = Interfaces.isPrivate; //tslint:disable-line:match-default-export-name
function createMessage(this: any, type: MessageType, sender: Character, text: string, time?: Date): Message {
if(type === MessageType.Message && isAction(text)) {
@ -536,10 +535,16 @@ class State implements Interfaces.State {
this.channelConversations.some((x) => x.unread === Interfaces.UnreadState.Mention);
}
getPrivate(character: Character): PrivateConversation {
getPrivate(character: Character): PrivateConversation;
getPrivate(character: Character, noCreate: boolean = false): PrivateConversation | undefined {
const key = character.name.toLowerCase();
let conv = state.privateMap[key];
if(conv !== undefined) return conv;
if (noCreate) {
return;
}
conv = new PrivateConversation(character);
this.privateConversations.push(conv);
this.privateMap[key] = conv;
@ -613,6 +618,55 @@ function isOfInterest(this: any, character: Character): boolean {
return character.isFriend || character.isBookmarked || state.privateMap[character.name.toLowerCase()] !== undefined;
}
async function testSmartFilterForPrivateMessage(fromChar: Character.Character): Promise<boolean> {
const cachedProfile = core.cache.profileCache.getSync(fromChar.name) || await core.cache.profileCache.get(fromChar.name);
if (
cachedProfile &&
cachedProfile.match.isFiltered &&
core.state.settings.risingFilter.autoReply &&
!cachedProfile.match.autoResponded
) {
cachedProfile.match.autoResponded = true;
log.debug('filter.autoresponse', { name: fromChar.name });
void Conversation.conversationThroat(
async() => {
await Conversation.testPostDelay();
// tslint:disable-next-line:prefer-template
const m = '[Automated message] Sorry, the player of this character has indicated that they are not interested in characters matching your profile. They will not see your message.\n\n' +
'Need a filter for yourself? Try out [url=https://mrstallion.github.io/fchat-rising/]F-Chat Rising[/url]';
core.connection.send('PRI', {recipient: fromChar.name, message: m});
core.cache.markLastPostTime();
}
);
}
if (cachedProfile && cachedProfile.match.isFiltered && core.state.settings.risingFilter.hidePrivateMessages) {
return true;
}
return false;
}
async function testSmartFilterForChannel(fromChar: Character.Character, conversation: ChannelConversation): Promise<boolean> {
if (
(isChannel(conversation) && conversation.channel.owner === '' && core.state.settings.risingFilter.hidePublicChannelMessages) ||
(isChannel(conversation) && conversation.channel.owner !== '' && core.state.settings.risingFilter.hidePrivateChannelMessages)
) {
const cachedProfile = core.cache.profileCache.getSync(fromChar.name) || await core.cache.profileCache.get(fromChar.name);
if (cachedProfile && cachedProfile.match.isFiltered && !fromChar.isChatOp) {
return true;
}
}
return false;
}
export default function(this: any): Interfaces.State {
state = new State();
window.addEventListener('focus', () => {
@ -679,12 +733,17 @@ export default function(this: any): Interfaces.State {
await conv.addMessage(new EventMessage(text));
}
});
connection.onMessage('PRI', async(data, time) => {
const char = core.characters.get(data.character);
if(char.isIgnored) return connection.send('IGN', {action: 'notify', character: data.character});
const message = createMessage(MessageType.Message, char, decodeHTML(data.message), time);
if (await testSmartFilterForPrivateMessage(char) === true) {
return;
}
EventBus.$emit('private-message', { message });
const conv = state.getPrivate(char);
await conv.addMessage(message);
});
@ -695,40 +754,14 @@ export default function(this: any): Interfaces.State {
if(char.isIgnored) return;
const message = createMessage(MessageType.Message, char, decodeHTML(data.message), time);
EventBus.$emit('channel-message', { message, channel: conversation });
await conversation.addMessage(message);
// message.type === MessageType.Message
if (
(isPrivate(conversation) && core.state.settings.risingFilter.hidePrivateMessages) ||
(isChannel(conversation) && conversation.channel.owner === '' && core.state.settings.risingFilter.hidePublicChannelMessages) ||
(isChannel(conversation) && conversation.channel.owner !== '' && core.state.settings.risingFilter.hidePrivateChannelMessages)
) {
const cachedProfile = core.cache.profileCache.getSync(char.name) || await core.cache.profileCache.get(char.name);
if (cachedProfile && isPrivate(conversation) && core.state.settings.risingFilter.autoReply && !cachedProfile.match.autoResponded) {
cachedProfile.match.autoResponded = true;
log.debug('filter.autoresponse', { name: char.name });
void Conversation.conversationThroat(
async() => {
await Conversation.testPostDelay();
// tslint:disable-next-line:prefer-template
const m = '[Automated message] Sorry, the player of this character has indicated that they are not interested in characters matching your profile. They will not see your message.\n\n' +
'Need a filter for yourself? Try out [url=https://mrstallion.github.io/fchat-rising/]F-Chat Rising[/url]';
core.connection.send('PRI', {recipient: char.name, message: m});
core.cache.markLastPostTime();
}
);
}
if (cachedProfile && cachedProfile.match.isFiltered) {
return;
}
if (await testSmartFilterForChannel(char, conversation) === true) {
return;
}
await conversation.addMessage(message);
EventBus.$emit('channel-message', { message, channel: conversation });
const words = conversation.settings.highlightWords.slice();
if(conversation.settings.defaultHighlights) words.push(...core.state.settings.highlightWords);
if(conversation.settings.highlight === Interfaces.Setting.Default && core.state.settings.highlight ||

View File

@ -26,6 +26,14 @@
<match-tags v-if="match" :match="match"></match-tags>
<div class="filter-matches" v-if="smartFilterIsFiltered">
<h4>Smart Filter Matches</h4>
<span class="tags">
<span v-for="filterName in smartFilterDetails" class="smart-filter-tag" :class="filterName">{{ (smartFilterLabels[filterName] || {}).name }}</span>
</span>
</div>
<!-- <div v-if="customs">-->
<!-- <span v-for="c in customs" :class="Score.getClasses(c.score)">{{c.name}}</span>-->
<!-- </div>-->
@ -72,6 +80,8 @@ import {
import { BBCodeView } from '../../bbcode/view';
import { EventBus } from './event-bus';
import { Character, CustomKink } from '../../interfaces';
import { matchesSmartFilters, testSmartFilters } from '../../learn/filter/smart-filter';
import { smartFilterTypes } from '../../learn/filter/types';
interface CustomKinkWithScore extends CustomKink {
score: number;
@ -97,6 +107,15 @@ export default class CharacterPreview extends Vue {
latestAd?: AdCachedPosting;
statusMessage?: string;
smartFilterIsFiltered?: boolean;
smartFilterDetails?: string[];
smartFilterLabels: Record<string, { name: string }> = {
...smartFilterTypes,
ageMin: { name: 'Min age' },
ageMax: { name: 'Max age' }
};
age?: string;
sexualOrientation?: string;
species?: string;
@ -170,6 +189,9 @@ export default class CharacterPreview extends Vue {
this.customs = undefined;
this.ownCharacter = core.characters.ownProfile;
this.smartFilterIsFiltered = false;
this.smartFilterDetails = [];
this.updateOnlineStatus();
this.updateAdStatus();
@ -177,11 +199,36 @@ export default class CharacterPreview extends Vue {
this.character = await this.getCharacterData(characterName);
this.match = Matcher.identifyBestMatchReport(this.ownCharacter!.character, this.character!.character);
this.updateSmartFilterReport();
this.updateCustoms();
this.updateDetails();
}, 0);
}
updateSmartFilterReport() {
if (!this.character) {
return;
}
this.smartFilterIsFiltered = matchesSmartFilters(this.character.character, core.state.settings.risingFilter);
this.smartFilterDetails = [];
if (!this.smartFilterIsFiltered) {
return;
}
const results = testSmartFilters(this.character.character, core.state.settings.risingFilter);
if (!results) {
return;
}
this.smartFilterDetails = [
..._.map(_.filter(_.toPairs(results.ageCheck), (v) => v[1]), (v) => v[0]),
..._.map(_.filter(_.toPairs(results.filters), (v) => v[1].isFiltered), (v: any) => v[0])
];
}
updateOnlineStatus(): void {
this.onlineCharacter = core.characters.get(this.characterName!);
@ -384,7 +431,8 @@ export default class CharacterPreview extends Vue {
}
.status-message,
.latest-ad-message {
.latest-ad-message,
.filter-matches {
display: block;
background-color: rgba(0,0,0,0.2);
padding: 10px;
@ -392,6 +440,23 @@ export default class CharacterPreview extends Vue {
margin-top: 1.3rem;
}
.filter-matches {
.tags {
margin-top: 10px;
display: block;
}
.smart-filter-tag {
display: inline-block;
color: var(--messageTimeFgColor);
margin-right: 4px;
background-color: var(--messageTimeBgColor);
border-radius: 2px;
padding-left: 3px;
padding-right: 3px;
}
}
.character-avatar {
width: 100%;
height: auto;

View File

@ -50,7 +50,7 @@ theme: jekyll-theme-slate
changelog: https://github.com/mrstallion/fchat-rising/blob/master/CHANGELOG.md
download:
version: 1.16.2
version: 1.17.0
url: https://github.com/mrstallion/fchat-rising/releases/download/v%VERSION%/F-Chat-Rising-%VERSION%-%PLATFORM_TAIL%
@ -58,14 +58,19 @@ download:
- type: win
name: Windows
tail: win.exe
size: 85 MB
size: 84 MB
- type: mac
name: MacOS
tail: macos.dmg
size: 82 MB
name: MacOS (Intel)
tail: macos-intel.dmg
size: 80 MB
instructions: ./macos-install
- type: mac
name: MacOS (M1)
tail: macos-m1.dmg
size: 83 MB
- type: linux
name: Linux
tail: linux.AppImage

View File

@ -53,7 +53,8 @@ require('electron-packager')({
icon: path.join(__dirname, 'build', 'icon'),
ignore: ['\.map$'],
osxSign: process.argv.length > 2 ? {identity: process.argv[2]} : false,
prune: false
prune: false,
arch: process.platform === 'darwin' ? ['x64', 'arm64'] : undefined
}).then((appPaths) => {
if (process.env.SKIP_INSTALLER) {
return;
@ -84,33 +85,38 @@ require('electron-packager')({
}).catch((e) => console.error(`Error while creating installer: ${e.message}`));
} else if(process.platform === 'darwin') {
console.log('Creating Mac DMG');
const target = path.join(distDir, `F-Chat Rising.dmg`);
if(fs.existsSync(target)) fs.unlinkSync(target);
const appPath = path.join(appPaths[0], 'F-Chat.app');
if(process.argv.length <= 2) console.warn('Warning: Creating unsigned DMG');
require('appdmg')({
basepath: appPaths[0],
target,
specification: {
title: 'F-Chat Rising',
icon: path.join(__dirname, 'build', 'icon.png'),
background: path.join(__dirname, 'build', 'dmg.png'),
contents: [{x: 555, y: 345, type: 'link', path: '/Applications'}, {x: 555, y: 105, type: 'file', path: appPath}],
'code-sign': process.argv.length > 2 ? {
'signing-identity': process.argv[2]
} : undefined
}
}).on('error', console.error);
const zipName = `F-Chat_Rising_${pkg.version}.zip`;
const zipPath = path.join(distDir, zipName);
if(fs.existsSync(zipPath)) fs.unlinkSync(zipPath);
const child = child_process.spawn('zip', ['-r', '-y', '-9', zipPath, 'F-Chat.app'], {cwd: appPaths[0]});
child.stdout.on('data', () => {});
child.stderr.on('data', (data) => console.error(data.toString()));
fs.writeFileSync(path.join(distDir, 'updates.json'), JSON.stringify({
releases: [{version: pkg.version, updateTo: {url: 'https://client.f-list.net/darwin/' + zipName}}],
currentRelease: pkg.version
}));
_.each([{ name: 'Intel', path: appPaths[0] }, { name: 'M1', path: appPaths[1] }], (arch) => {
console.log(arch.name, arch.path);
const target = path.join(distDir, `F-Chat Rising ${arch.name}.dmg`);
if(fs.existsSync(target)) fs.unlinkSync(target);
const appPath = path.join(arch.path, 'F-Chat.app');
if(process.argv.length <= 2) console.warn('Warning: Creating unsigned DMG');
require('appdmg')({
basepath: arch.path,
target,
specification: {
title: 'F-Chat Rising',
icon: path.join(__dirname, 'build', 'icon.png'),
background: path.join(__dirname, 'build', 'dmg.png'),
contents: [{x: 555, y: 345, type: 'link', path: '/Applications'}, {x: 555, y: 105, type: 'file', path: appPath}],
'code-sign': process.argv.length > 2 ? {
'signing-identity': process.argv[2]
} : undefined
}
}).on('error', console.error);
const zipName = `F-Chat_Rising_${arch.name}_${pkg.version}.zip`;
const zipPath = path.join(distDir, zipName);
if(fs.existsSync(zipPath)) fs.unlinkSync(zipPath);
const child = child_process.spawn('zip', ['-r', '-y', '-9', zipPath, 'F-Chat.app'], {cwd: arch.path});
child.stdout.on('data', () => {});
child.stderr.on('data', (data) => console.error(data.toString()));
fs.writeFileSync(path.join(distDir, 'updates.json'), JSON.stringify({
releases: [{version: pkg.version, updateTo: {url: 'https://client.f-list.net/darwin/' + zipName}}],
currentRelease: pkg.version
}));
});
} else {
console.log('Creating Linux AppImage');
fs.renameSync(path.join(appPaths[0], 'F-Chat'), path.join(appPaths[0], 'AppRun'));

View File

@ -1,6 +1,6 @@
{
"name": "fchat",
"version": "1.16.2",
"version": "1.17.0",
"author": "The F-List Team and Mister Stallion (Esq.)",
"description": "F-List.net Chat Client",
"main": "main.js",

View File

@ -1,6 +1,6 @@
import _ from 'lodash';
import { Matcher } from '../matcher';
import { BodyType, Build, Kink, Species, TagId } from '../matcher-types';
import { BodyType, Build, Gender, Kink, Species, TagId } from '../matcher-types';
import { SmartFilterSelection, SmartFilterSettings } from './types';
import { Character } from '../../interfaces';
import log from 'electron-log';
@ -8,32 +8,44 @@ import core from '../../chat/core';
export interface SmartFilterOpts {
name: string;
kinks?: Kink[],
bodyTypes?: BodyType[],
builds?: Build[],
species?: Species[]
kinks?: Kink[];
bodyTypes?: BodyType[];
builds?: Build[];
species?: Species[];
genders?: Gender[];
isAnthro?: boolean;
isHuman?: boolean;
}
export interface SmartFilterTestResult {
isFiltered: boolean;
builds: boolean;
bodyTypes: boolean;
species: boolean;
genders: boolean;
isAnthro: boolean;
isHuman: boolean;
kinks: boolean;
}
export class SmartFilter {
constructor(private opts: SmartFilterOpts) {}
test(c: Character): boolean {
test(c: Character): SmartFilterTestResult {
const builds = this.testBuilds(c);
const bodyTypes = this.testBodyTypes(c);
const species = this.testSpecies(c);
const isAnthro = this.testIsAnthro(c);
const isHuman = this.testIsHuman(c);
const kinks = this.testKinks(c);
const genders = this.testGenders(c);
const result = builds || bodyTypes || species || isAnthro || isHuman || kinks;
const isFiltered = builds || bodyTypes || species || isAnthro || isHuman || kinks || genders;
const result = { isFiltered, builds, bodyTypes, species, isAnthro, isHuman, kinks, genders };
log.debug('smart-filter.test',
{ name: c.name, filterName: this.opts.name, result, builds, bodyTypes, species, isAnthro, isHuman, kinks });
log.silly('smart-filter.test', { name: c.name, filterName: this.opts.name, result });
return this.testBuilds(c) || this.testBodyTypes(c) || this.testSpecies(c) || this.testIsAnthro(c) || this.testIsHuman(c) ||
this.testKinks(c);
return result;
}
testKinks(c: Character): boolean {
@ -65,6 +77,16 @@ export class SmartFilter {
return !!build && !!_.find(this.opts.builds || [], build);
}
testGenders(c: Character): boolean {
if (!this.opts.genders) {
return false;
}
const gender = Matcher.getTagValueList(TagId.Gender, c);
return !!gender && !!_.find(this.opts.genders || [], gender);
}
testBodyTypes(c: Character): boolean {
if (!this.opts.bodyTypes) {
return false;
@ -94,11 +116,11 @@ export class SmartFilter {
}
}
export type SmartFilterCollection = {
[key in keyof SmartFilterSelection]: SmartFilter;
};
export const smartFilters: SmartFilterCollection = {
ageplay: new SmartFilter({
name: 'ageplay',
@ -110,6 +132,11 @@ export const smartFilters: SmartFilterCollection = {
isAnthro: true
}),
female: new SmartFilter({
name: 'female',
genders: [Gender.Female]
}),
feral: new SmartFilter({
name: 'feral',
bodyTypes: [BodyType.Feral]
@ -140,6 +167,16 @@ export const smartFilters: SmartFilterCollection = {
kinks: [Kink.Incest, Kink.IncestParental, Kink.IncestSiblings, Kink.ParentChildPlay, Kink.ForcedIncest]
}),
intersex: new SmartFilter({
name: 'intersex',
genders: [Gender.Transgender, Gender.Herm, Gender.MaleHerm, Gender.Cuntboy, Gender.Shemale]
}),
male: new SmartFilter({
name: 'male',
genders: [Gender.Male]
}),
microMacro: new SmartFilter({
name: 'microMacro',
kinks: [Kink.MacroAsses, Kink.MacroBalls, Kink.MacroBreasts, Kink.MacroCocks, Kink.Macrophilia, Kink.MegaMacro, Kink.Microphilia,
@ -205,31 +242,59 @@ export const smartFilters: SmartFilterCollection = {
})
};
export function matchesSmartFilters(c: Character, opts: SmartFilterSettings): boolean {
export function testSmartFilters(c: Character, opts: SmartFilterSettings): {
ageCheck: { ageMin: boolean; ageMax: boolean };
filters: { [key in keyof SmartFilterCollection]: SmartFilterTestResult }
} | null {
if (c.name === core.characters.ownCharacter.name) {
return false;
return null;
}
if (core.characters.get(c.name)?.isChatOp) {
return false;
const coreCharacter = core.characters.get(c.name);
if (coreCharacter?.isChatOp || coreCharacter?.isBookmarked || coreCharacter?.isFriend) {
return null;
}
if (opts.exceptionNames.includes(c.name)) {
log.debug('smart-filter.exception', { name: c.name });
return false;
return null;
}
const ageCheck = { ageMin: false, ageMax: false };
if (opts.minAge !== null || opts.maxAge !== null) {
const age = Matcher.age(c);
const age = Matcher.age(c) || Matcher.apparentAge(c)?.min || null;
if (age !== null) {
if ((opts.minAge !== null && age < opts.minAge) || (opts.maxAge !== null && age > opts.maxAge)) {
log.debug('smart-filter.age', { name: c.name, age, minAge: opts.minAge, maxAge: opts.maxAge });
return true;
if (opts.minAge !== null && age < opts.minAge) {
log.debug('smart-filter.age.min', { name: c.name, age, minAge: opts.minAge });
ageCheck.ageMin = true;
}
if (opts.maxAge !== null && age > opts.maxAge) {
log.debug('smart-filter.age.max', { name: c.name, age, maxAge: opts.maxAge });
ageCheck.ageMax = true;
}
}
}
return !_.every(opts.smartFilters, (fs, filterName) => !fs || !(smartFilters as any)[filterName].test(c));
return {
ageCheck,
filters: _.mapValues(smartFilters, (f, k) => (opts.smartFilters as any)[k] && f.test(c))
};
}
export function matchesSmartFilters(c: Character, opts: SmartFilterSettings): boolean {
const match = testSmartFilters(c, opts);
if (!match) {
return false;
}
if (match.ageCheck.ageMax || match.ageCheck.ageMin) {
return true;
}
return !_.every(match.filters, (filterResult) => !filterResult.isFiltered);
}

View File

@ -1,15 +1,14 @@
// <!-- [Automated message] Sorry, the player of this character has indicated that they are not interested in characters matching your profile.-->
// <!-- Need a filter for yourself? Try out [F-Chat Rising](https://mrstallion.github.io/fchat-rising/)!-->
export const smartFilterTypes = {
ageplay: { name: 'Ageplay' },
anthro: { name: 'Anthros' },
female: { name: 'Females' },
feral: { name: 'Ferals' },
gore: { name: 'Gore/torture/death' },
human: { name: 'Humans' },
hyper: { name: 'Hyper' },
incest: { name: 'Incest' },
intersex: { name: 'Intersex' },
male: { name: 'Males' },
microMacro: { name: 'Micro/macro' },
obesity: { name: 'Obesity' },
pokemon: { name: 'Pokemons/Digimons' },
@ -36,7 +35,9 @@ export interface SmartFilterSettings {
hidePrivateChannelMessages: boolean;
hidePrivateMessages: boolean;
penalizeMatches: boolean;
rewardNonMatches: boolean;
autoReply: boolean;
showFilterIcon: boolean;
minAge: number | null;
maxAge: number | null;

View File

@ -1237,11 +1237,59 @@ export class Matcher {
static age(c: Character): number | null {
const rawAge = Matcher.getTagValue(TagId.Age, c);
const age = ((rawAge) && (rawAge.string)) ? parseInt(rawAge.string, 10) : null;
if (!rawAge || !rawAge.string) {
return null;
}
const ageStr = rawAge.string.toLowerCase().trim();
if ((ageStr.indexOf('shota') >= 0) || (ageStr.indexOf('loli') >= 0) || (ageStr.indexOf('lolli') >= 0)) {
return 10;
}
const age = parseInt(rawAge.string, 10);
return age && !Number.isNaN(age) && Number.isFinite(age) ? age : null;
}
static apparentAge(c: Character): { min: number, max: number } | null {
const rawAge = Matcher.getTagValue(TagId.ApparentAge, c);
if ((!rawAge) || (!rawAge.string)) {
return null;
}
const ageStr = rawAge.string.trim().toLowerCase();
if (ageStr === '') {
return null;
}
// '18'
if (/^[0-9]+$/.exec(ageStr)) {
const val = parseInt(rawAge.string, 10);
return { min: val, max: val };
}
// '18-22'
const rangeMatch = ageStr.match(/^([0-9]+)-([0-9]+)$/);
if (rangeMatch) {
const v1 = parseInt(rangeMatch[1], 10);
const v2 = parseInt(rangeMatch[2], 10);
return { min: Math.min(v1, v2), max: Math.max(v1, v2) };
}
if ((ageStr.indexOf('shota') >= 0) || (ageStr.indexOf('loli') >= 0) || (ageStr.indexOf('lolli') >= 0)) {
return { min: 10, max: 10 };
}
return null;
}
static calculateSearchScoreForMatch(
score: Scoring,
match: MatchReport,

View File

@ -161,10 +161,11 @@ export class ProfileCache extends AsyncCache<CharacterCacheRecord> {
// const totalScoreDimensions = match ? Matcher.countScoresTotal(match) : 0;
// const dimensionsAtScoreLevel = match ? (Matcher.countScoresAtLevel(match, score) || 0) : 0;
// const dimensionsAboveScoreLevel = match ? (Matcher.countScoresAboveLevel(match, Math.max(score, Scoring.WEAK_MATCH))) : 0;
const isFiltered = matchesSmartFilters(c.character, core.state.settings.risingFilter);
const risingFilter = core.state.settings.risingFilter;
const isFiltered = matchesSmartFilters(c.character, risingFilter);
const searchScore = match
? Matcher.calculateSearchScoreForMatch(score, match, isFiltered && core.state.settings.risingFilter.penalizeMatches ? -2 : 0)
? Matcher.calculateSearchScoreForMatch(score, match, (isFiltered && risingFilter.penalizeMatches) ? -2 : (!isFiltered && risingFilter.rewardNonMatches) ? 1 : 0)
: 0;
const matchDetails = { matchScore: score, searchScore, isFiltered };

View File

@ -1,6 +1,6 @@
{
"name": "f-list-rising",
"version": "1.16.2",
"version": "1.17.0",
"author": "The F-List Team and and Mister Stallion (Esq.)",
"description": "A heavily modded F-Chat 3.0 client for F-List",
"license": "MIT",