Skip to content

Commit

Permalink
Entitlement resets
Browse files Browse the repository at this point in the history
  • Loading branch information
Excellify committed Oct 30, 2024
1 parent 5da39d5 commit 947407f
Show file tree
Hide file tree
Showing 12 changed files with 155 additions and 67 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG-nightly.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
### 3.1.5.1000

- Entitlements like paints and badges should now better reflect what the user has equipped
- Fixed and issue with entitlements on accounts with multiple connections

### 3.1.4.1000

- Fixed an issue with avatars not loading
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"displayName": "7TV",
"description": "Improve your viewing experience on Twitch & YouTube with new features, emotes, vanity and performance.",
"private": true,
"version": "3.1.4",
"version": "3.1.5",
"dev_version": "1.0",
"scripts": {
"start": "NODE_ENV=dev yarn build:dev && NODE_ENV=dev vite --mode dev",
Expand Down
2 changes: 1 addition & 1 deletion src/app/chat/UserTag.vue
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ watchEffect(() => {
const t = Date.now();
const stop = watch(
[cosmetics.paints, cosmetics.badges],
[() => cosmetics.paints, () => cosmetics.badges],
([paints, badges]) => {
// condition to ignore
// msg is not the last message, or is older than a second
Expand Down
122 changes: 78 additions & 44 deletions src/composable/useCosmetics.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ComputedRef, computed, reactive, toRef, watch } from "vue";
import { ComputedRef, computed, reactive, ref, toRef, watch } from "vue";
import { until, useTimeout } from "@vueuse/core";
import { DecimalToStringRGBA } from "@/common/Color";
import { log } from "@/common/Logger";
Expand Down Expand Up @@ -59,6 +59,7 @@ class CosmeticMap<T extends SevenTV.CosmeticKind> extends Map<string, SevenTV.Co
}

let flushTimeout: number | null = null;
const firstFlush = ref(false);

/**
* Set up cosmetics
Expand Down Expand Up @@ -104,6 +105,7 @@ db.ready().then(async () => {
kind: "BADGE",
ref_id: badge.id,
user_id: u,
platform_id: u,
},
"+",
);
Expand All @@ -129,6 +131,7 @@ db.ready().then(async () => {
kind: "PAINT",
ref_id: paint.id,
user_id: u,
platform_id: u,
},
"+",
);
Expand All @@ -152,19 +155,19 @@ db.ready().then(async () => {
* @param mode "+" to bind, "-" to unbind
*/
function setEntitlement(ent: SevenTV.Entitlement, mode: "+" | "-") {
if (data.staticallyAssigned[ent.user_id]) {
if (data.staticallyAssigned[ent.platform_id]) {
// If user had statically assigned cosmetics,
// clear them so they be properly set with live data
for (const cos of data.userBadges[ent.user_id]?.values() ?? []) {
for (const cos of data.userBadges[ent.platform_id]?.values() ?? []) {
if (cos.provider !== "7TV") continue;

data.userBadges[ent.user_id].delete(cos.id);
data.userBadges[ent.platform_id].delete(cos.id);
}
for (const cos of data.userPaints[ent.user_id]?.values() ?? []) {
data.userPaints[ent.user_id].delete(cos.id);
for (const cos of data.userPaints[ent.platform_id]?.values() ?? []) {
data.userPaints[ent.platform_id].delete(cos.id);
}

delete data.staticallyAssigned[ent.user_id];
delete data.staticallyAssigned[ent.platform_id];
}

data.entitlementBuffers[mode].push(ent);
Expand All @@ -177,31 +180,42 @@ db.ready().then(async () => {
// This operation processes a time gap between grants and revokations
// in order to allow the UI to update smoothly
function flush() {
firstFlush.value = true;
if (flushTimeout) return;

flushTimeout = window.setTimeout(() => {
const add = data.entitlementBuffers["+"].splice(0, data.entitlementBuffers["+"].length);
const del = data.entitlementBuffers["-"].splice(0, data.entitlementBuffers["-"].length);
const add = new Map(
data.entitlementBuffers["+"].splice(0, data.entitlementBuffers["+"].length).map((e) => [e.id, e]),
);
const del = new Map(
data.entitlementBuffers["-"].splice(0, data.entitlementBuffers["-"].length).map((e) => [e.id, e]),
);

for (const ent of del) {
for (const [id, ent] of del.entries()) {
// if we are removing entitlement and adding it back we can skip it entirely
if (add.has(id)) {
add.delete(id);
continue;
}
const l = userListFor(ent.kind);
if (!l[ent.user_id] || !l[ent.user_id].has(ent.ref_id)) continue;
if (!l[ent.platform_id] || !l[ent.platform_id].has(ent.ref_id)) continue;

l[ent.user_id].delete(ent.ref_id);
l[ent.platform_id].delete(ent.ref_id);
}

flushTimeout = window.setTimeout(async () => {
for (const ent of add) {
for (const ent of add.values()) {
const l = userListFor(ent.kind);

if (ent.kind === "EMOTE_SET") {
if (!l[ent.user_id]) l[ent.user_id] = new Map();
if (!l[ent.platform_id]) l[ent.platform_id] = new Map();

bindUserEmotes(ent.user_id, ent.ref_id);
bindUserEmotes(ent.platform_id, ent.ref_id);
} else {
if (!l[ent.user_id]) (l[ent.user_id] as CosmeticMap<"BADGE" | "PAINT">) = new CosmeticMap();
if (!l[ent.platform_id])
(l[ent.platform_id] as CosmeticMap<"BADGE" | "PAINT">) = new CosmeticMap();

const m = l[ent.user_id] as CosmeticMap<"BADGE" | "PAINT">;
const m = l[ent.platform_id] as CosmeticMap<"BADGE" | "PAINT">;
awaitCosmetic(ent.ref_id).then((cos) => {
if (m.has(ent.ref_id) || m.hasProvider(cos.provider)) return;

Expand Down Expand Up @@ -281,38 +295,58 @@ db.ready().then(async () => {
target.addEventListener("entitlement_deleted", (ev) => {
setEntitlement(ev.detail, "-");
});
target.addEventListener("entitlement_reset", async (ev) => {
const userID = ev.detail.id;
await until(firstFlush).toBe(true);

const entries = db.entitlements
.where("user_id")
.equals(userID)
.and((e) => e.kind !== "EMOTE_SET");

await entries.each((ent) => {
setEntitlement(ent, "-");
});

entries.delete();
});

// Assign stored entitlements
db.entitlements.toArray().then((ents) => {
for (const ent of ents) {
let assigned = false;

const isLegacy = !!data.staticallyAssigned[ent.user_id];
switch (ent.kind) {
case "BADGE":
if (!isLegacy && data.userBadges[ent.user_id]?.size) continue;

setEntitlement(ent, "+");
assigned = true;
break;
case "PAINT":
if (!isLegacy && data.userPaints[ent.user_id]?.size) continue;

setEntitlement(ent, "+");
assigned = true;
break;
case "EMOTE_SET":
bindUserEmotes(ent.user_id, ent.ref_id);
break;
}
db.entitlements
.toArray()
.then((ents) => {
for (const ent of ents) {
let assigned = false;

const id = ent.platform_id ?? ent.user_id;

const isLegacy = !!data.staticallyAssigned[id];
switch (ent.kind) {
case "BADGE":
if (!isLegacy && data.userBadges[id]?.size) continue;

setEntitlement(ent, "+");
assigned = true;
break;
case "PAINT":
if (!isLegacy && data.userPaints[id]?.size) continue;

setEntitlement(ent, "+");
assigned = true;
break;
case "EMOTE_SET":
bindUserEmotes(id, ent.ref_id);
break;
}

log.debug("<Cosmetics>", "Assigned", ents.length.toString(), "stored entitlements");
log.debug("<Cosmetics>", "Assigned", ents.length.toString(), "stored entitlements");

if (assigned) {
data.staticallyAssigned[ent.user_id] = {};
if (assigned) {
data.staticallyAssigned[ent.user_id] = {};
}
}
}
});
})
.then(flush);
});

export function useCosmetics(userID: string) {
Expand Down
12 changes: 10 additions & 2 deletions src/composable/useWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,12 @@ function useHandlers(mp: MessagePort) {
events.emit("entitlement_deleted", entitlement);
break;
}
case "ENTITLEMENT_RESET": {
const entitlement = data as TypedWorkerMessage<"ENTITLEMENT_RESET">;

events.emit("entitlement_reset", entitlement);
break;
}
case "STATIC_COSMETICS_FETCHED": {
const { badges, paints } = data as TypedWorkerMessage<"STATIC_COSMETICS_FETCHED">;

Expand Down Expand Up @@ -249,6 +255,7 @@ export type WorkletEventName =
| "cosmetic_created"
| "entitlement_created"
| "entitlement_deleted"
| "entitlement_reset"
| "static_cosmetics_fetched"
| "twitch_emote_set_data"
| "emote_set_updated"
Expand All @@ -261,8 +268,9 @@ type WorkletTypedEvent<EVN extends WorkletEventName> = {
channel_fetched: CurrentChannel;
channel_sets_fetched: CurrentChannel;
cosmetic_created: Pick<SevenTV.Cosmetic, "id" | "data" | "kind">;
entitlement_created: Pick<SevenTV.Entitlement, "id" | "kind" | "ref_id" | "user_id">;
entitlement_deleted: Pick<SevenTV.Entitlement, "id" | "kind" | "ref_id" | "user_id">;
entitlement_created: Pick<SevenTV.Entitlement, "id" | "kind" | "ref_id" | "user_id" | "platform_id">;
entitlement_deleted: Pick<SevenTV.Entitlement, "id" | "kind" | "ref_id" | "user_id" | "platform_id">;
entitlement_reset: Pick<SevenTV.Entitlement, "id">;
static_cosmetics_fetched: Pick<TypedWorkerMessage<"STATIC_COSMETICS_FETCHED">, "badges" | "paints">;
twitch_emote_set_data: SevenTV.EmoteSet;
emote_set_updated: TypedWorkerMessage<"EMOTE_SET_UPDATED">;
Expand Down
11 changes: 9 additions & 2 deletions src/db/idb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export class Dexie7 extends Dexie {
emoteSets!: Table<SevenTV.EmoteSet & WithTimestamp, SevenTV.ObjectID>;
emotes!: Table<SevenTV.Emote & WithTimestamp, SevenTV.ObjectID>;
cosmetics!: Table<SevenTV.Cosmetic & WithTimestamp, SevenTV.ObjectID>;
entitlements!: Table<SevenTV.Entitlement & ScopedEntitlement & WithTimestamp, SevenTV.ObjectID>;
entitlements!: Table<SevenTV.Entitlement & ScopedEntitlement & WithTimestamp & WithPlatform, SevenTV.ObjectID>;
settings!: Table<SevenTV.Setting<SevenTV.SettingType>>;

constructor() {
Expand Down Expand Up @@ -123,7 +123,10 @@ export class Dexie7 extends Dexie {
// Clean up entitlements
this.entitlements
.filter(
(e) => !e.scope || e.scope.split(",").every((v) => !exemptChannels.includes(v.split(":")[1] ?? "")),
(e) =>
!e.platform_id ||
!e.scope ||
e.scope.split(",").every((v) => !exemptChannels.includes(v.split(":")[1] ?? "")),
)
.delete();

Expand Down Expand Up @@ -163,3 +166,7 @@ interface Transaction<T> {
interface ScopedEntitlement {
scope?: string;
}

interface WithPlatform {
platform_id?: string;
}
8 changes: 8 additions & 0 deletions src/db/versions.idb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,12 @@ export function defineVersions(db: Dexie7) {
entitlements: "id,scope,timestamp,user_id",
settings: "key",
});
db.version(2.6).stores({
channels: "id,timestamp",
emoteSets: "id,timestamp,priority,provider,scope",
emotes: "id,timestamp,name,owner.id",
cosmetics: "id,timestamp,kind",
entitlements: "id,scope,timestamp,user_id,platform_id",
settings: "key",
});
}
1 change: 1 addition & 0 deletions src/types/app.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ declare namespace SevenTV {
user?: User;
user_id: ObjectID;
ref_id: ObjectID;
platform_id: string;
}

type UserType = "" | "BOT" | "SYSTEM";
Expand Down
43 changes: 32 additions & 11 deletions src/worker/event-handlers/entitlement.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,26 @@ export async function onEntitlementCreate(ctx: EventContext, cm: ChangeMap<Seven
if (!obj || !obj.user || !obj.user.connections?.length) return;

const ids = obj.user.connections.filter((x) => x.platform === platform).map((x) => x.id);
const user_id = obj.user.id;
delete obj.user;
ids.forEach((cid) => {
// Write to IDB
ctx.db.entitlements
.put({
...obj,
scope: port.channelIds.map((channelID) => `${platform}:${channelID ?? "X"}`).join(","),
user_id: cid,
})
.catch(() => ctx.db.entitlements.update(obj.id, obj));
obj.id = `${cid}:${obj.kind}:${obj.ref_id}`;
const o = {
...obj,
scope: port.channelIds.map((channelID) => `${platform}:${channelID ?? "X"}`).join(","),
user_id: user_id,
platform_id: cid,
};
ctx.db.entitlements.put(o).catch(() => ctx.db.entitlements.update(o.id, o));

// Send the entitlement to the client
port.postMessage("ENTITLEMENT_CREATED", {
id: obj.id,
kind: obj.kind,
ref_id: obj.ref_id,
user_id: cid,
user_id: user_id,
platform_id: cid,
});
});
}
Expand All @@ -47,16 +50,34 @@ export async function onEntitlementDelete(ctx: EventContext, cm: ChangeMap<Seven
.filter((x) => x.platform === platform)
.map((x) => x.id)
.forEach((cid) => {
const id = `${cid}:${obj.kind}:${obj.ref_id}`;
// Write to IDB
ctx.db.entitlements.delete(obj.id);
ctx.db.entitlements.delete(id);

// Send the entitlement to the client
port.postMessage("ENTITLEMENT_DELETED", {
id: obj.id,
id: id,
kind: obj.kind,
ref_id: obj.ref_id,
user_id: cid,
user_id: obj.user!.id,
platform_id: cid,
});
});
}
}

export async function onEntitlementReset(ctx: EventContext, obj: Pick<SevenTV.User, "id">) {
for (const port of ctx.driver.ports.values()) {
const platform = port.platform;
if (!platform) return; // no platform set

const o: typeof obj = structuredClone(obj);
if (!o.id) return;

port.postMessage("ENTITLEMENT_RESET", {
id: o.id,
});

ctx.db.entitlements.filter((e) => e.user_id === o.id).delete();
}
}
3 changes: 2 additions & 1 deletion src/worker/event-handlers/handler.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { log } from "@/common/Logger";
import { onCosmeticCreate } from "./cosmetic.handler";
import { onEmoteSetCreate, onEmoteSetUpdate } from "./emote-set.handler";
import { onEntitlementCreate, onEntitlementDelete } from "./entitlement.handler";
import { onEntitlementCreate, onEntitlementDelete, onEntitlementReset } from "./entitlement.handler";
import { onUserUpdate } from "./user.handler";
import type { ChangeField, ChangeMap, EventContext, ObjectTypeOfKind } from "../";

Expand All @@ -13,6 +13,7 @@ export function handleDispatchedEvent(ctx: EventContext, type: string, cm: Chang
onEntitlementCreate(ctx, structuredClone(cm) as ChangeMap<SevenTV.ObjectKind.ENTITLEMENT>),
"entitlement.delete": () =>
onEntitlementDelete(ctx, structuredClone(cm) as ChangeMap<SevenTV.ObjectKind.ENTITLEMENT>),
"entitlement.reset": () => onEntitlementReset(ctx, structuredClone(cm) as Pick<SevenTV.User, "id">),
"emote_set.create": () => onEmoteSetCreate(ctx, cm as ChangeMap<SevenTV.ObjectKind.EMOTE_SET>),
"emote_set.update": () => onEmoteSetUpdate(ctx, cm as ChangeMap<SevenTV.ObjectKind.EMOTE_SET>),
}[type];
Expand Down
Loading

0 comments on commit 947407f

Please sign in to comment.