Latest messages in character preview; better filters; log filtered messages

This commit is contained in:
Mr. Stallion 2022-03-25 18:53:37 -07:00
parent f160b0f176
commit 75b5ef54cf
13 changed files with 169 additions and 34 deletions

View File

@ -1,9 +1,14 @@
# Changelog
## 1.18.0
* Upgraded to Electron 18
* Upgraded to Electron 17
* Fixed MacOS M1 incompatibilities
* Improved age detection
* Taur and feral body types are now matched against kink preferences
* Filtered messages are now accessible in the conversation history
* Rejection messages are now also sent to filtered characters whose profiles have not been scored at the time they message you
* Slightly relaxed filter scoring
* Character preview now shows last messages from conversation history
## 1.17.1
* Fixes to smart filters

View File

@ -1,8 +1,8 @@
# Download
[Windows](https://github.com/mrstallion/fchat-rising/releases/download/v1.17.1/F-Chat-Rising-1.17.1-win.exe) (82 MB)
| [MacOS Intel](https://github.com/mrstallion/fchat-rising/releases/download/v1.17.1/F-Chat-Rising-1.17.1-macos-intel.dmg) (82 MB)
| [MacOS M1](https://github.com/mrstallion/fchat-rising/releases/download/v1.17.1/F-Chat-Rising-1.17.1-macos-m1.dmg) (84 MB)
| [Linux](https://github.com/mrstallion/fchat-rising/releases/download/v1.17.1/F-Chat-Rising-1.17.1-linux.AppImage) (82 MB)
[Windows](https://github.com/mrstallion/fchat-rising/releases/download/v1.18.0/F-Chat-Rising-1.18.0-win.exe) (82 MB)
| [MacOS Intel](https://github.com/mrstallion/fchat-rising/releases/download/v1.18.0/F-Chat-Rising-1.18.0-macos-intel.dmg) (82 MB)
| [MacOS M1](https://github.com/mrstallion/fchat-rising/releases/download/v1.18.0/F-Chat-Rising-1.18.0-macos-m1.dmg) (84 MB)
| [Linux](https://github.com/mrstallion/fchat-rising/releases/download/v1.18.0/F-Chat-Rising-1.18.0-linux.AppImage) (82 MB)
# F-Chat Rising

View File

@ -618,8 +618,23 @@ 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> {
async function withNeutralVisibilityPrivateConversation(
character: Character.Character,
cb: (p: PrivateConversation, c: Character.Character) => Promise<void>
): Promise<void> {
const isVisibleConversation = !!(state.getPrivate as any)(character, true);
const conv = state.getPrivate(character);
await cb(conv, character);
if (!isVisibleConversation) {
await conv.close();
}
}
export async function testSmartFilterForPrivateMessage(fromChar: Character.Character, originalMessage?: Message): Promise<boolean> {
const cachedProfile = core.cache.profileCache.getSync(fromChar.name) || await core.cache.profileCache.get(fromChar.name);
const firstTime = cachedProfile && !cachedProfile.match.autoResponded;
if (
cachedProfile &&
@ -629,23 +644,51 @@ async function testSmartFilterForPrivateMessage(fromChar: Character.Character):
) {
cachedProfile.match.autoResponded = true;
log.debug('filter.autoresponse', { name: fromChar.name });
void Conversation.conversationThroat(
async() => {
log.debug('filter.autoresponse', { name: fromChar.name });
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]';
// tslint:disable-next-line:prefer-template
const message = {
recipient: fromChar.name,
message: '\n[sub][color=orange][b][AUTOMATED MESSAGE][/b][/color][/sub]\n' +
'Sorry, the player of this character is not interested in characters matching your profile.' +
`${core.state.settings.risingFilter.hidePrivateMessages ? ' They did not see your message. To bypass this warning, send your message again.' : ''}\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.connection.send('PRI', message);
core.cache.markLastPostTime();
if (core.state.settings.logMessages) {
const logMessage = createMessage(Interfaces.Message.Type.Message, core.characters.ownCharacter,
message.message, new Date());
await withNeutralVisibilityPrivateConversation(
fromChar,
async(p) => core.logs.logMessage(p, logMessage)
);
}
}
);
}
if (cachedProfile && cachedProfile.match.isFiltered && core.state.settings.risingFilter.hidePrivateMessages) {
if (
cachedProfile &&
cachedProfile.match.isFiltered &&
core.state.settings.risingFilter.hidePrivateMessages &&
firstTime // subsequent messages bypass this filter on purpose
) {
if (core.state.settings.logMessages && originalMessage) {
await withNeutralVisibilityPrivateConversation(
fromChar,
async(p) => core.logs.logMessage(p, originalMessage)
);
}
return true;
}
@ -738,7 +781,7 @@ export default function(this: any): Interfaces.State {
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) {
if (await testSmartFilterForPrivateMessage(char, message) === true) {
return;
}

View File

@ -92,7 +92,9 @@ export namespace Conversation {
readonly selectedConversation: Conversation
readonly hasNew: boolean;
byKey(key: string): Conversation | undefined
getPrivate(character: Character): PrivateConversation
getPrivate(character: Character): PrivateConversation;
getPrivate(character: Character, noCreate: boolean): PrivateConversation | undefined;
}
export enum Setting {

View File

@ -43,6 +43,15 @@
<bbcode :text="statusMessage"></bbcode>
</div>
<div class="conversation" v-if="conversation && conversation.length > 0">
<h4>Latest Messages</h4>
<template v-for="message in conversation">
<message-view :message="message" :key="message.id">
</message-view>
</template>
</div>
<div class="latest-ad-message" v-if="latestAd && (latestAd.message !== statusMessage)">
<h4>Latest Ad <span class="message-time">{{formatTime(latestAd.datePosted)}}</span></h4>
<bbcode :text="latestAd.message"></bbcode>
@ -82,6 +91,8 @@ import { EventBus } from './event-bus';
import { Character, CustomKink } from '../../interfaces';
import { matchesSmartFilters, testSmartFilters } from '../../learn/filter/smart-filter';
import { smartFilterTypes } from '../../learn/filter/types';
import { Conversation } from '../interfaces';
import MessageView from '../message_view';
interface CustomKinkWithScore extends CustomKink {
score: number;
@ -91,7 +102,8 @@ interface CustomKinkWithScore extends CustomKink {
@Component({
components: {
'match-tags': MatchTags,
bbcode: BBCodeView(core.bbCodeParser)
bbcode: BBCodeView(core.bbCodeParser),
'message-view': MessageView
}
})
export default class CharacterPreview extends Vue {
@ -132,6 +144,8 @@ export default class CharacterPreview extends Vue {
scoreWatcher: ((event: any) => void) | null = null;
customs?: CustomKinkWithScore[];
conversation?: Conversation.Message[];
@Hook('mounted')
mounted(): void {
@ -189,6 +203,8 @@ export default class CharacterPreview extends Vue {
this.customs = undefined;
this.ownCharacter = core.characters.ownProfile;
this.conversation = undefined;
this.smartFilterIsFiltered = false;
this.smartFilterDetails = [];
@ -199,6 +215,8 @@ export default class CharacterPreview extends Vue {
this.character = await this.getCharacterData(characterName);
this.match = Matcher.identifyBestMatchReport(this.ownCharacter!.character, this.character!.character);
void this.updateConversationStatus();
this.updateSmartFilterReport();
this.updateCustoms();
this.updateDetails();
@ -229,6 +247,25 @@ export default class CharacterPreview extends Vue {
];
}
async updateConversationStatus(): Promise<void> {
const char = core.characters.get(this.characterName!);
if (char) {
const messages = await core.logs.getLogs(core.characters.ownCharacter.name, char.name.toLowerCase(), new Date());
const matcher = /\[AUTOMATED MESSAGE]/;
this.conversation = _.map(
_.takeRight(_.filter(messages, (m) => !matcher.exec(m.text)), 3),
(m) => ({
...m,
text: m.text.length > 512 ? m.text.substr(0, 512) + '…' : m.text
})
);
// this.conversation = core.conversations.getPrivate(char, true);
}
}
updateOnlineStatus(): void {
this.onlineCharacter = core.characters.get(this.characterName!);
@ -432,6 +469,7 @@ export default class CharacterPreview extends Vue {
.status-message,
.latest-ad-message,
.conversation,
.filter-matches {
display: block;
background-color: rgba(0,0,0,0.2);

View File

@ -50,7 +50,7 @@ theme: jekyll-theme-slate
changelog: https://github.com/mrstallion/fchat-rising/blob/master/CHANGELOG.md
download:
version: 1.17.1
version: 1.18.0
url: https://github.com/mrstallion/fchat-rising/releases/download/v%VERSION%/F-Chat-Rising-%VERSION%-%PLATFORM_TAIL%

View File

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

View File

@ -21,7 +21,8 @@ import { PermanentIndexedStore } from './store/types';
import * as path from 'path';
// import * as electron from 'electron';
import log from 'electron-log'; //tslint:disable-line:match-default-export-name
import log from 'electron-log';
import { testSmartFilterForPrivateMessage } from '../chat/conversations'; //tslint:disable-line:match-default-export-name
export interface ProfileCacheQueueEntry {
@ -132,8 +133,21 @@ export class CacheManager {
);
this.populateAllConversationsWithScore(c.character.name, score, isFiltered);
void this.respondToPendingRejections(c);
}
// Manage rejections in case we didn't have a score at the time we received the message
async respondToPendingRejections(c: ComplexCharacter): Promise<void> {
const char = core.characters.get(c.character.name);
if (char && char.status !== 'offline') {
const conv = core.conversations.getPrivate(char, true);
if (conv && conv.messages.length > 0 && Date.now() - _.last(conv.messages)!.time.getTime() < 3 * 60 * 1000) {
await testSmartFilterForPrivateMessage(char);
}
}
}
async addProfile(character: string | ComplexCharacter): Promise<void> {
if (typeof character === 'string') {

View File

@ -1,4 +1,4 @@
import _ from 'lodash';
import * as _ from 'lodash';
import { Matcher } from '../matcher';
import { BodyType, Build, Gender, Kink, Species, TagId } from '../matcher-types';
import { SmartFilterSelection, SmartFilterSettings } from './types';
@ -28,6 +28,10 @@ export interface SmartFilterTestResult {
kinks: boolean;
}
function getBaseLog(base: number, x: number): number {
return Math.log(x) / Math.log(base);
}
export class SmartFilter {
constructor(private opts: SmartFilterOpts) {}
@ -64,7 +68,10 @@ export class SmartFilter {
return curScore;
}, { score: 0, matches: 0 });
return score.matches >= 1 && score.score >= 1.0 + (Math.log((this.opts.kinks?.length || 0) + 1) / 2);
const baseLog = getBaseLog(5, (this.opts.kinks?.length || 0) + 1);
const threshold = (baseLog * baseLog) + 1;
return score.matches >= 1 && score.score >= threshold;
}
testBuilds(c: Character): boolean {

View File

@ -241,7 +241,9 @@ export enum Kink {
Microphilia = 286,
SizeDifferencesMicroMacro = 502,
GrowthMacro = 384,
ShrinkingMicro = 387
ShrinkingMicro = 387,
Taurs = 68
}
export enum FurryPreference {
@ -341,6 +343,16 @@ export const genderKinkMapping: GenderKinkIdMap = {
[Gender.Transgender]: Kink.Transgenders
};
export interface BodyTypeKinkIdMap {
[key: number]: Kink
}
export const bodyTypeKinkMapping: BodyTypeKinkIdMap = {
[BodyType.Feral]: Kink.AnimalsFerals,
[BodyType.Taur]: Kink.Taurs
};
// if no species and 'no furry characters', === human
// if no species and dislike 'anthro characters' === human
@ -468,7 +480,7 @@ export const speciesMapping: SpeciesMap = {
],
[Species.Human]: ['human', 'homo sapiens', 'human.*', 'homo[ -]?sapi[ea]ns?', 'woman', 'hy?[uo]+m[aie]n', 'humaine?',
'meat[ -]?popsicle',
'meat[ -]?popsicle'
],
[Species.Elf]: ['drow', 'draenei', 'dunmer', 'draenai', 'blutelf[e]?', 'elf.*', 'drow.*', 'e[ -]l[ -]f', 'sin\'?dorei',

View File

@ -10,7 +10,7 @@ import anyAscii from 'any-ascii';
import { Store } from '../site/character_page/data_store';
import {
BodyType,
BodyType, bodyTypeKinkMapping,
fchatGenderMap,
FurryPreference,
Gender,
@ -151,6 +151,7 @@ export class CharacterAnalysis {
readonly subDomRole: SubDomRole | null;
readonly position: Position | null;
readonly postLengthPreference: PostLengthPreference | null;
readonly bodyType: BodyType | null;
readonly isAnthro: boolean | null;
readonly isHuman: boolean | null;
@ -166,10 +167,9 @@ export class CharacterAnalysis {
this.subDomRole = Matcher.getTagValueList(TagId.SubDomRole, c);
this.position = Matcher.getTagValueList(TagId.Position, c);
this.postLengthPreference = Matcher.getTagValueList(TagId.PostLength, c);
this.bodyType = Matcher.getTagValueList(TagId.BodyType, c);
const ageTag = Matcher.getTagValue(TagId.Age, c);
this.age = ((ageTag) && (ageTag.string)) ? parseInt(ageTag.string, 10) : null;
this.age = Matcher.age(c);
this.isAnthro = Matcher.isAnthro(c);
this.isHuman = Matcher.isHuman(c);
@ -405,7 +405,8 @@ export class Matcher {
[TagId.SubDomRole]: this.resolveSubDomScore(),
[TagId.Kinks]: this.resolveKinkScore(pronoun),
[TagId.PostLength]: this.resolvePostLengthScore(),
[TagId.Position]: this.resolvePositionScore()
[TagId.Position]: this.resolvePositionScore(),
[TagId.BodyType]: this.resolveBodyTypeScore()
},
info: {
@ -723,6 +724,19 @@ export class Matcher {
return new Score(Scoring.NEUTRAL);
}
private resolveBodyTypeScore(): Score {
const theirBodyType = Matcher.getTagValueList(TagId.BodyType, this.them);
if (theirBodyType && theirBodyType in bodyTypeKinkMapping) {
const bodyTypePreference = Matcher.getKinkPreference(this.you, bodyTypeKinkMapping[theirBodyType]);
if (bodyTypePreference !== null) {
return Matcher.formatKinkScore(bodyTypePreference, `{BodyType[theirBodyType].toLowerCase()}s`);
}
}
return new Score(Scoring.NEUTRAL);
}
private resolveSubDomScore(): Score {
const you = this.you;
@ -954,7 +968,6 @@ export class Matcher {
return result;
}
// private countKinksByBucket(kinks: { [key: number]: KinkChoice }): { favorite: number, yes: number, maybe: number, no: number } {
// return _.reduce(
// kinks,
@ -971,7 +984,6 @@ export class Matcher {
// );
// }
private getAllStandardKinks(c: Character): { [key: number]: KinkChoice } {
const kinks = _.pickBy(c.kinks, _.isString);
@ -1244,7 +1256,8 @@ export class Matcher {
const ageStr = rawAge.string.toLowerCase().replace(/[,.]/g, '').trim();
if ((ageStr.indexOf('shota') >= 0) || (ageStr.indexOf('loli') >= 0) || (ageStr.indexOf('lolli') >= 0) || (ageStr.indexOf('pup') >= 0)) {
if ((ageStr.indexOf('shota') >= 0) || (ageStr.indexOf('loli') >= 0)
|| (ageStr.indexOf('lolli') >= 0) || (ageStr.indexOf('pup') >= 0)) {
return 10;
}
@ -1297,7 +1310,8 @@ export class Matcher {
return { min: Math.min(v1, v2), max: Math.max(v1, v2) };
}
if ((ageStr.indexOf('shota') >= 0) || (ageStr.indexOf('loli') >= 0) || (ageStr.indexOf('lolli') >= 0) || (ageStr.indexOf('pup') >= 0)) {
if ((ageStr.indexOf('shota') >= 0) || (ageStr.indexOf('loli') >= 0)
|| (ageStr.indexOf('lolli') >= 0) || (ageStr.indexOf('pup') >= 0)) {
return { min: 10, max: 10 };
}

View File

@ -1,6 +1,6 @@
{
"name": "f-list-rising",
"version": "1.17.1",
"version": "1.18.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",

View File

@ -63,7 +63,7 @@
}
yourInterestIsRelevant(id: number): boolean {
return ((id === TagId.Gender) || (id === TagId.Age) || (id === TagId.Species));
return ((id === TagId.Gender) || (id === TagId.Age) || (id === TagId.Species) || (id === TagId.BodyType));
}
get contactLink(): string | undefined {