Skip to content

Commit

Permalink
move all emote & cheermote parsing logic out of common package
Browse files Browse the repository at this point in the history
  • Loading branch information
d-fischer committed Apr 20, 2023
1 parent 38083cc commit da20b3a
Show file tree
Hide file tree
Showing 19 changed files with 196 additions and 350 deletions.
54 changes: 54 additions & 0 deletions packages/api/src/endpoints/bits/CheermoteDisplayInfo.ts
@@ -0,0 +1,54 @@
/**
* The type of background a cheermote is supposed to appear on.
*
* We will supply a fitting graphic that does not show any artifacts
* on the given type of background.
*/
export type CheermoteBackground = 'dark' | 'light';

/**
* The state of a cheermote, i.e. whether it's animated or not.
*/
export type CheermoteState = 'animated' | 'static';

/**
* The scale of the cheermote, which usually relates to the pixel density of the device in use.
*/
export type CheermoteScale = '1' | '1.5' | '2' | '3' | '4';

/**
* The format of the cheermote you want to request.
*/
export interface CheermoteFormat {
/**
* The desired background for the cheermote.
*/
background: CheermoteBackground;

/**
* The desired cheermote state.
*/
state: CheermoteState;

/**
* The desired cheermote scale.
*/
scale: CheermoteScale;
}

/**
* The details on how a cheermote should be displayed.
*/
export interface CheermoteDisplayInfo {
/**
* The URL of the image that should be shown.
*/
url: string;

/**
* The color that should be used to show the cheer amount.
*
* This is a hexadecimal color value, e.g. `#9c3ee8`.
*/
color: string;
}
6 changes: 3 additions & 3 deletions packages/api/src/endpoints/bits/HelixCheermoteList.ts
@@ -1,15 +1,15 @@
import { indexBy } from '@d-fischer/shared-utils';
import type { CheermoteDisplayInfo, CheermoteFormat } from '@twurple/common';
import { BaseCheermoteList, HellFreezesOverError, rawDataSymbol, rtfm } from '@twurple/common';
import { DataObject, HellFreezesOverError, rawDataSymbol, rtfm } from '@twurple/common';
import { type HelixCheermoteData } from '../../interfaces/endpoints/bits.external';
import { type CheermoteDisplayInfo, type CheermoteFormat } from './CheermoteDisplayInfo';

/**
* A list of cheermotes you can use globally or in a specific channel, depending on how you fetched the list.
*
* @inheritDoc
*/
@rtfm('api', 'HelixCheermoteList')
export class HelixCheermoteList extends BaseCheermoteList<Record<string, HelixCheermoteData>> {
export class HelixCheermoteList extends DataObject<Record<string, HelixCheermoteData>> {
/** @private */
constructor(data: HelixCheermoteData[]) {
super(indexBy(data, action => action.prefix.toLowerCase()));
Expand Down
6 changes: 1 addition & 5 deletions packages/api/src/index.ts
Expand Up @@ -221,13 +221,9 @@ export type { HelixPaginatedResult, HelixPaginatedResultWithTotal } from './util
export type { HelixForwardPagination, HelixPagination } from './utils/pagination/HelixPagination';

export type { HelixResponse, HelixPaginatedResponse, HelixPaginatedResponseWithTotal } from '@twurple/api-call';
export { ChatEmote, extractUserId, extractUserName, HelixExtension, HellFreezesOverError } from '@twurple/common';
export { extractUserId, extractUserName, HelixExtension, HellFreezesOverError } from '@twurple/common';
export type {
CheermoteBackground,
CheermoteDisplayInfo,
CommercialLength,
CheermoteScale,
CheermoteState,
HelixExtensionSubscriptionsSupportLevel,
HelixExtensionState,
HelixExtensionIconSize,
Expand Down
6 changes: 5 additions & 1 deletion packages/api/src/interfaces/endpoints/bits.external.ts
@@ -1,5 +1,9 @@
import { type HelixResponse } from '@twurple/api-call';
import { type CheermoteBackground, type CheermoteScale, type CheermoteState } from '@twurple/common';
import {
type CheermoteBackground,
type CheermoteScale,
type CheermoteState
} from '../../endpoints/bits/CheermoteDisplayInfo';
import { type HelixBitsLeaderboardQuery } from './bits.input';
import { type HelixDateRangeData } from './generic.external';

Expand Down
@@ -1,10 +1,7 @@
import type { ParsedMessagePart } from '@twurple/common';
import { parseChatMessage } from '@twurple/common';
import type { MessageParam } from 'ircv3';
import { Message, MessageParamDefinition, MessageType } from 'ircv3';
import { ChatUser } from '../../../ChatUser';
import { parseEmoteOffsets } from '../../../utils/emoteUtil';
import { getMessageText } from '../../../utils/messageUtil';

@MessageType('USERNOTICE')
export class UserNotice extends Message<UserNotice> {
Expand Down Expand Up @@ -39,10 +36,4 @@ export class UserNotice extends Message<UserNotice> {
get emoteOffsets(): Map<string, string[]> {
return parseEmoteOffsets(this._tags.get('emotes'));
}

parseEmotes(): ParsedMessagePart[] {
const messageText = getMessageText(this.params.message);

return parseChatMessage(messageText, this.emoteOffsets) as ParsedMessagePart[];
}
}
9 changes: 0 additions & 9 deletions packages/chat/src/caps/twitchCommands/messageTypes/Whisper.ts
@@ -1,10 +1,7 @@
import type { ParsedMessagePart } from '@twurple/common';
import { parseChatMessage } from '@twurple/common';
import type { MessageParam } from 'ircv3';
import { Message, MessageParamDefinition, MessageType } from 'ircv3';
import { ChatUser } from '../../../ChatUser';
import { parseEmoteOffsets } from '../../../utils/emoteUtil';
import { getMessageText } from '../../../utils/messageUtil';

/** @private */
@MessageType('WHISPER')
Expand All @@ -24,10 +21,4 @@ export class Whisper extends Message<Whisper> {
get emoteOffsets(): Map<string, string[]> {
return parseEmoteOffsets(this._tags.get('emotes'));
}

parseEmotes(): ParsedMessagePart[] {
const messageText = getMessageText(this.params.message);

return parseChatMessage(messageText, this.emoteOffsets) as ParsedMessagePart[];
}
}
89 changes: 89 additions & 0 deletions packages/chat/src/emotes/ParsedMessagePart.ts
@@ -0,0 +1,89 @@
/**
* A part of a parsed message that represents plain text.
*/
export interface ParsedMessageTextPart {
/**
* The type of the message part. This is text.
*/
type: 'text';

/**
* The starting position of the text in the message, zero based.
*/
position: number;

/**
* The length of the text in the message.
*/
length: number;

/**
* The text.
*/
text: string;
}

/**
* A part of a parsed message that represents a cheermote.
*/
export interface ParsedMessageCheerPart {
/**
* The type of the message part. This is a cheermote.
*/
type: 'cheer';

/**
* The name of the cheermote.
*/
name: string;

/**
* The amount of bits for the cheermote.
*/
amount: number;

/**
* The starting position of the cheermote in the message, zero based.
*/
position: number;

/**
* The length of the cheermote in the message.
*/
length: number;
}

/**
* A part of a parsed message that represents an emote.
*/
export interface ParsedMessageEmotePart {
/**
* The type of the message part. This is an emote.
*/
type: 'emote';

/**
* The starting position of the emote in the message, zero based.
*/
position: number;

/**
* The length of the emote in the message.
*/
length: number;

/**
* The ID of the emote.
*/
id: string;

/**
* The name of the emote.
*/
name: string;
}

/**
* A part of a parsed message.
*/
export type ParsedMessagePart = ParsedMessageTextPart | ParsedMessageCheerPart | ParsedMessageEmotePart;
@@ -1,4 +1,3 @@
import { ChatEmote } from '../ChatEmote';
import { parseChatMessage } from '../messagePartParser';

describe('Message part parser', () => {
Expand Down Expand Up @@ -31,11 +30,7 @@ describe('Message part parser', () => {
id: '25',
name: 'Kappa',
position: 25,
length: 5,
displayInfo: new ChatEmote({
id: '25',
code: 'Kappa'
})
length: 5
},
{
type: 'text',
Expand Down Expand Up @@ -76,11 +71,7 @@ describe('Message part parser', () => {
id: '305954156',
name: 'PogChamp',
position: 23,
length: 8,
displayInfo: new ChatEmote({
id: '305954156',
code: 'PogChamp'
})
length: 8
},
{
type: 'text',
Expand Down Expand Up @@ -128,11 +119,7 @@ describe('Message part parser', () => {
id: '305954156',
name: 'PogChamp',
position: 27,
length: 8,
displayInfo: new ChatEmote({
id: '305954156',
code: 'PogChamp'
})
length: 8
},
{
type: 'text',
Expand Down
@@ -1,11 +1,5 @@
import { utf8Length, utf8Substring } from '@d-fischer/shared-utils';
import { type BasicMessageCheermote } from './BaseCheermoteList';
import { ChatEmote } from './ChatEmote';
import {
type BasicParsedMessageCheerPart,
type BasicParsedMessagePart,
type ParsedMessageEmotePart
} from './ParsedMessagePart';
import { type ParsedMessageCheerPart, type ParsedMessageEmotePart, type ParsedMessagePart } from './ParsedMessagePart';

/**
* Transforms the parts of the given text that are marked as emotes.
Expand All @@ -27,11 +21,7 @@ export function parseEmotePositions(text: string, emoteOffsets: Map<string, stri
position: start,
length: end - start + 1,
id: emote,
name,
displayInfo: new ChatEmote({
code: name,
id: emote
})
name
};
})
)
Expand All @@ -44,8 +34,8 @@ export function parseEmotePositions(text: string, emoteOffsets: Map<string, stri
* @param text The message text.
* @param names The names of the cheermotes to find.
*/
export function findCheermotePositions(text: string, names: string[]): BasicMessageCheermote[] {
const result: BasicMessageCheermote[] = [];
export function findCheermotePositions(text: string, names: string[]): ParsedMessageCheerPart[] {
const result: ParsedMessageCheerPart[] = [];

const re = new RegExp('(?<=^|\\s)([a-z]+(?:\\d+[a-z]+)*)(\\d+)(?=\\s|$)', 'gi');
let match: RegExpExecArray | null = null;
Expand All @@ -54,6 +44,7 @@ export function findCheermotePositions(text: string, names: string[]): BasicMess
if (names.includes(name)) {
const amount = Number(match[2]);
result.push({
type: 'cheer',
name,
amount,
position: utf8Length(text.slice(0, match.index)),
Expand All @@ -71,7 +62,7 @@ export function findCheermotePositions(text: string, names: string[]): BasicMess
* @param text The message text.
* @param otherPositions The parsed non-text parts of the message.
*/
export function fillTextPositions(text: string, otherPositions: BasicParsedMessagePart[]): BasicParsedMessagePart[] {
export function fillTextPositions(text: string, otherPositions: ParsedMessagePart[]): ParsedMessagePart[] {
const messageLength = utf8Length(text);
if (!otherPositions.length) {
return [
Expand All @@ -84,7 +75,7 @@ export function fillTextPositions(text: string, otherPositions: BasicParsedMessa
];
}

const result: BasicParsedMessagePart[] = [];
const result: ParsedMessagePart[] = [];
let currentPosition = 0;

for (const token of otherPositions) {
Expand Down Expand Up @@ -123,23 +114,15 @@ export function parseChatMessage(
text: string,
emoteOffsets: Map<string, string[]>,
cheermoteNames?: string[]
): BasicParsedMessagePart[] {
): ParsedMessagePart[] {
if (!text) {
return [];
}

const foundParts: BasicParsedMessagePart[] = parseEmotePositions(text, emoteOffsets);
const foundParts: ParsedMessagePart[] = parseEmotePositions(text, emoteOffsets);

if (cheermoteNames) {
foundParts.push(
...findCheermotePositions(text, cheermoteNames).map(
(cm): BasicParsedMessageCheerPart => ({
type: 'cheer',
...cm
})
)
);

foundParts.push(...findCheermotePositions(text, cheermoteNames));
foundParts.sort((a, b) => a.position - b.position);
}

Expand Down
15 changes: 14 additions & 1 deletion packages/chat/src/index.ts
Expand Up @@ -38,6 +38,19 @@ export { ChatUser } from './ChatUser';

export { LogLevel } from '@d-fischer/logger';

export { parseTwitchMessage } from './utils/messageUtil';
export {
findCheermotePositions,
fillTextPositions,
parseChatMessage,
parseEmotePositions
} from './emotes/messagePartParser';
export {
type ParsedMessagePart,
type ParsedMessageEmotePart,
type ParsedMessageCheerPart,
type ParsedMessageTextPart
} from './emotes/ParsedMessagePart';

export { parseTwitchMessage, extractMessageText } from './utils/messageUtil';
export { parseEmoteOffsets } from './utils/emoteUtil';
export { toChannelName, toUserName } from './utils/userUtil';

0 comments on commit da20b3a

Please sign in to comment.