fchat-rising/learn/profile-cache.ts

267 lines
7.5 KiB
TypeScript

import * as _ from 'lodash';
import core from '../chat/core';
import {Character as ComplexCharacter, CharacterGroup, Guestbook} from '../site/character_page/interfaces';
import { AsyncCache } from './async-cache';
import { Matcher, MatchReport } from './matcher';
import { PermanentIndexedStore } from './store/types';
import { CharacterImage, SimpleCharacter } from '../interfaces';
import { Scoring } from './matcher-types';
import { matchesSmartFilters } from './filter/smart-filter';
import * as remote from '@electron/remote';
export interface MetaRecord {
images: CharacterImage[] | null;
groups: CharacterGroup[] | null;
friends: SimpleCharacter[] | null;
guestbook: Guestbook | null;
lastMetaFetched: Date | null;
}
export interface CountRecord {
groupCount: number | null;
friendCount: number | null;
guestbookCount: number | null;
lastCounted: number | null;
}
export interface CharacterMatchSummary {
matchScore: number;
// dimensionsAtScoreLevel: number;
// dimensionsAboveScoreLevel: number;
// totalScoreDimensions: number;
searchScore: number;
isFiltered: boolean;
autoResponded?: boolean;
}
export interface CharacterCacheRecord {
character: ComplexCharacter;
lastFetched: Date;
added: Date;
// counts?: CountRecord;
match: CharacterMatchSummary;
meta?: MetaRecord;
}
export class ProfileCache extends AsyncCache<CharacterCacheRecord> {
protected store?: PermanentIndexedStore;
protected lastFetch = Date.now();
setStore(store: PermanentIndexedStore): void {
this.store = store;
}
onEachInMemory(cb: (c: CharacterCacheRecord, key: string) => void): void {
_.each(this.cache, cb);
}
getSync(name: string): CharacterCacheRecord | null {
const key = AsyncCache.nameKey(name);
if (key in this.cache) {
return this.cache[key];
}
return null;
}
async get(name: string, skipStore: boolean = false, fromChannel?: string): Promise<CharacterCacheRecord | null> {
const key = AsyncCache.nameKey(name);
if (key in this.cache) {
return this.cache[key];
}
if ((!this.store) || (skipStore)) {
return null;
}
if (false) {
console.log(`Retrieve '${name}' for channel '${fromChannel}, gap: ${(Date.now() - this.lastFetch)}ms`);
this.lastFetch = Date.now();
}
const pd = await this.store.getProfile(name);
if (!pd) {
return null;
}
const cacheRecord = await this.register(pd.profileData, true);
cacheRecord.lastFetched = new Date(pd.lastFetched * 1000);
cacheRecord.added = new Date(pd.firstSeen * 1000);
cacheRecord.meta = {
lastMetaFetched: pd.lastMetaFetched ? new Date(pd.lastMetaFetched * 1000) : null,
groups: pd.groups,
friends: pd.friends,
images: pd.images,
guestbook: pd.guestbook
};
/* cacheRecord.counts = {
lastCounted: pd.lastCounted,
groupCount: pd.groupCount,
friendCount: pd.friendCount,
guestbookCount: pd.guestbookCount
}; */
return cacheRecord;
}
// async registerCount(name: string, counts: CountRecord): Promise<void> {
// const record = await this.get(name);
//
// if (!record) {
// // coward's way out
// return;
// }
//
// record.counts = counts;
//
// if (this.store) {
// await this.store.updateProfileCounts(name, counts.guestbookCount, counts.friendCount, counts.groupCount);
// }
// }
async registerMeta(name: string, meta: MetaRecord): Promise<void> {
const record = await this.get(name);
if (!record) {
// coward's way out
return;
}
record.meta = meta;
if (this.store) {
await this.store.updateProfileMeta(name, meta.images, meta.guestbook, meta.friends, meta.groups);
}
}
isSafeImageURL(url: string): boolean {
if (url.match(/^https?:\/\/static\.f-list\.net\//i)) {
return true;
}
if (url.match(/^https?:\/\/([a-z0-9\-.]+\.)?imgur\.com\//i)) {
return true;
}
if (url.match(/^https?:\/\/([a-z0-9\-.]+\.)?freeimage\.host\//i)) {
return true;
}
if (url.match(/^https?:\/\/([a-z0-9\-.]+\.)?iili\.io\//i)) {
return true;
}
if (url.match(/^https?:\/\/([a-z0-9\-.]+\.)?redgifs\.com\//i)) {
return true;
}
if (url.match(/^https?:\/\/([a-z0-9\-.]+\.)?e621\.net\//i)) {
return true;
}
return false;
}
updateOverrides(c: ComplexCharacter): void {
const match = c.character.description.match(/\[url=(.*?)]\s*?Rising\s*?Portrait\s*?\[\/url]/i);
if (match && match[1]) {
const avatarUrl = match[1].trim();
if (!this.isSafeImageURL(avatarUrl)) {
return;
}
if (c.character.name === core.characters.ownCharacter.name) {
const parent = remote.getCurrentWindow().webContents;
parent.send('update-avatar-url', c.character.name, avatarUrl);
}
core.characters.setOverride(c.character.name, 'avatarUrl', avatarUrl);
}
}
async register(c: ComplexCharacter, skipStore: boolean = false): Promise<CharacterCacheRecord> {
const k = AsyncCache.nameKey(c.character.name);
const match = ProfileCache.match(c);
let score = (!match || match.score === null) ? Scoring.NEUTRAL : match.score;
if (score === 0) {
console.log(`Storing score 0 for character ${c.character.name}`);
}
this.updateOverrides(c);
// 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 risingFilter = core.state.settings.risingFilter;
const isFiltered = matchesSmartFilters(c.character, risingFilter);
const penalty = (isFiltered && risingFilter.penalizeMatches) ? -5 : (!isFiltered && risingFilter.rewardNonMatches) ? 2 : 0;
if (isFiltered && risingFilter.penalizeMatches) {
score = Scoring.MISMATCH;
}
const searchScore = match
? Matcher.calculateSearchScoreForMatch(score, match, penalty)
: 0;
const matchDetails = { matchScore: score, searchScore, isFiltered };
if ((this.store) && (!skipStore)) {
await this.store.storeProfile(c);
}
if (k in this.cache) {
const rExisting = this.cache[k];
rExisting.character = c;
rExisting.lastFetched = new Date();
rExisting.match = matchDetails;
return rExisting;
}
const rNew = {
character: c,
lastFetched: new Date(),
added: new Date(),
match: matchDetails
};
this.cache[k] = rNew;
return rNew;
}
static match(c: ComplexCharacter): MatchReport | null {
const you = core.characters.ownProfile;
if (!you) {
return null;
}
return Matcher.identifyBestMatchReport(you.character, c.character);
}
}