chore: cleanup missionInventoryUpdateService (#349)
Co-authored-by: Sainan <Sainan@users.noreply.github.com>
This commit is contained in:
parent
56f9be9725
commit
079d67807a
8
package-lock.json
generated
8
package-lock.json
generated
@ -13,7 +13,7 @@
|
|||||||
"express": "^5.0.0-beta.3",
|
"express": "^5.0.0-beta.3",
|
||||||
"mongoose": "^8.1.1",
|
"mongoose": "^8.1.1",
|
||||||
"warframe-items": "^1.1262.74",
|
"warframe-items": "^1.1262.74",
|
||||||
"warframe-public-export-plus": "^0.2.5",
|
"warframe-public-export-plus": "^0.3.0",
|
||||||
"warframe-riven-info": "^0.1.0",
|
"warframe-riven-info": "^0.1.0",
|
||||||
"winston": "^3.11.0",
|
"winston": "^3.11.0",
|
||||||
"winston-daily-rotate-file": "^4.7.1"
|
"winston-daily-rotate-file": "^4.7.1"
|
||||||
@ -3678,9 +3678,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/warframe-public-export-plus": {
|
"node_modules/warframe-public-export-plus": {
|
||||||
"version": "0.2.5",
|
"version": "0.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.3.0.tgz",
|
||||||
"integrity": "sha512-IsS2Z14CeTpGSpfeUxqTi8wAQjQ6qjh2kV8RC9St5hcDmII3NpwEFXmStEqz7r+JPfea72D3cZMMl+4QLHqvXw=="
|
"integrity": "sha512-BYkTkCq9jsA8NzSiWsTW48ezK7kI/op2NrLf+j4j3bJi2cNjoSLf/D4bMEui6yCADjcoV89ramRTFbPjn6UpLA=="
|
||||||
},
|
},
|
||||||
"node_modules/warframe-riven-info": {
|
"node_modules/warframe-riven-info": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
"express": "^5.0.0-beta.3",
|
"express": "^5.0.0-beta.3",
|
||||||
"mongoose": "^8.1.1",
|
"mongoose": "^8.1.1",
|
||||||
"warframe-items": "^1.1262.74",
|
"warframe-items": "^1.1262.74",
|
||||||
"warframe-public-export-plus": "^0.2.5",
|
"warframe-public-export-plus": "^0.3.0",
|
||||||
"warframe-riven-info": "^0.1.0",
|
"warframe-riven-info": "^0.1.0",
|
||||||
"winston": "^3.11.0",
|
"winston": "^3.11.0",
|
||||||
"winston-daily-rotate-file": "^4.7.1"
|
"winston-daily-rotate-file": "^4.7.1"
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { getIndexAfter } from "@/src/helpers/stringHelpers";
|
import { getIndexAfter } from "@/src/helpers/stringHelpers";
|
||||||
import { logger } from "@/src/utils/logger";
|
import { logger } from "@/src/utils/logger";
|
||||||
import Items, { Category, MinimalItem, Warframe, Weapon } from "warframe-items";
|
import Items, { MinimalItem, Warframe, Weapon } from "warframe-items";
|
||||||
import badItems from "@/static/json/exclude-mods.json";
|
|
||||||
import {
|
import {
|
||||||
dict_en,
|
dict_en,
|
||||||
ExportRecipes,
|
ExportRecipes,
|
||||||
@ -53,23 +52,6 @@ export const getWeaponType = (weaponName: string): WeaponTypeInternal => {
|
|||||||
return weaponType;
|
return weaponType;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getNamesObj = (category: Category) =>
|
|
||||||
new Items({ category: [category] }).reduce<{ [index: string]: string }>((acc, item) => {
|
|
||||||
if (!(item.uniqueName! in badItems)) {
|
|
||||||
acc[item.name!.replace("'S", "'s")] = item.uniqueName!;
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
export const modNames = getNamesObj("Mods");
|
|
||||||
export const resourceNames = getNamesObj("Resources");
|
|
||||||
export const miscNames = getNamesObj("Misc");
|
|
||||||
export const relicNames = getNamesObj("Relics");
|
|
||||||
export const skinNames = getNamesObj("Skins");
|
|
||||||
export const arcaneNames = getNamesObj("Arcanes");
|
|
||||||
export const gearNames = getNamesObj("Gear");
|
|
||||||
//logger.debug(`gear names`, { gearNames });
|
|
||||||
|
|
||||||
export const craftNames = Object.fromEntries(
|
export const craftNames = Object.fromEntries(
|
||||||
(
|
(
|
||||||
new Items({
|
new Items({
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import { IMissionRewardResponse, IReward, IInventoryFieldType, inventoryFields } from "@/src/types/missionTypes";
|
import { IMissionRewardResponse, IInventoryFieldType, inventoryFields } from "@/src/types/missionTypes";
|
||||||
|
|
||||||
import missionsDropTable from "@/static/json/missions-drop-table.json";
|
|
||||||
import {
|
import {
|
||||||
modNames,
|
ExportRegions,
|
||||||
relicNames,
|
ExportRewards,
|
||||||
miscNames,
|
ExportUpgrades,
|
||||||
resourceNames,
|
ExportGear,
|
||||||
gearNames,
|
ExportRecipes,
|
||||||
blueprintNames
|
ExportRelics,
|
||||||
} from "@/src/services/itemDataService";
|
IReward
|
||||||
|
} from "warframe-public-export-plus";
|
||||||
import { IMissionInventoryUpdateRequest } from "../types/requestTypes";
|
import { IMissionInventoryUpdateRequest } from "../types/requestTypes";
|
||||||
import { logger } from "@/src/utils/logger";
|
import { logger } from "@/src/utils/logger";
|
||||||
|
|
||||||
@ -23,49 +23,25 @@ const getRewards = ({
|
|||||||
return { InventoryChanges: {}, MissionRewards: [] };
|
return { InventoryChanges: {}, MissionRewards: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
const rewards = (missionsDropTable as { [key: string]: IReward[] })[RewardInfo.node];
|
const rewardManifests = ExportRegions[RewardInfo.node]?.rewardManifests ?? [];
|
||||||
if (!rewards) {
|
if (rewardManifests.length == 0) {
|
||||||
return { InventoryChanges: {}, MissionRewards: [] };
|
return { InventoryChanges: {}, MissionRewards: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
const rotationCount = RewardInfo.rewardQualifications?.length || 0;
|
const rotationCount = RewardInfo.rewardQualifications?.length || 0;
|
||||||
const rotations = getRotations(rotationCount);
|
const rotations = getRotations(rotationCount);
|
||||||
const drops: IReward[] = [];
|
const drops: IReward[] = [];
|
||||||
for (const rotation of rotations) {
|
rewardManifests
|
||||||
const rotationRewards = rewards.filter(reward => reward.rotation === rotation);
|
.map(name => ExportRewards[name])
|
||||||
|
.forEach(table => {
|
||||||
// Separate guaranteed and chance drops
|
for (const rotation of rotations) {
|
||||||
const guaranteedDrops: IReward[] = [];
|
const rotationRewards = table[rotation];
|
||||||
const chanceDrops: IReward[] = [];
|
const drop = getRandomRewardByChance(rotationRewards);
|
||||||
for (const reward of rotationRewards) {
|
if (drop) {
|
||||||
if (reward.chance === 100) {
|
drops.push(drop);
|
||||||
guaranteedDrops.push(reward);
|
}
|
||||||
} else {
|
|
||||||
chanceDrops.push(reward);
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
const randomDrop = getRandomRewardByChance(chanceDrops);
|
|
||||||
if (randomDrop) {
|
|
||||||
guaranteedDrops.push(randomDrop);
|
|
||||||
}
|
|
||||||
|
|
||||||
drops.push(...guaranteedDrops);
|
|
||||||
}
|
|
||||||
|
|
||||||
// const testDrops = [
|
|
||||||
// { chance: 7.69, name: "Lith W3 Relic", rotation: "B" },
|
|
||||||
// { chance: 7.69, name: "Lith W3 Relic", rotation: "B" },
|
|
||||||
// { chance: 10.82, name: "2X Orokin Cell", rotation: "C" },
|
|
||||||
// { chance: 10.82, name: "Arrow Mutation", rotation: "C" },
|
|
||||||
// { chance: 10.82, name: "200 Endo", rotation: "C" },
|
|
||||||
// { chance: 10.82, name: "200 Endo", rotation: "C" },
|
|
||||||
// { chance: 10.82, name: "2,000,000 Credits Cache", rotation: "C" },
|
|
||||||
// { chance: 7.69, name: "Health Restore (Large)", rotation: "C" },
|
|
||||||
// { chance: 7.69, name: "Vapor Specter Blueprint", rotation: "C" }
|
|
||||||
// ];
|
|
||||||
// logger.debug("Mission rewards:", testDrops);
|
|
||||||
// return formatRewardsToInventoryType(testDrops);
|
|
||||||
|
|
||||||
logger.debug("Mission rewards:", drops);
|
logger.debug("Mission rewards:", drops);
|
||||||
return formatRewardsToInventoryType(drops);
|
return formatRewardsToInventoryType(drops);
|
||||||
@ -100,10 +76,10 @@ const combineRewardAndLootInventory = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRotations = (rotationCount: number): (string | undefined)[] => {
|
const getRotations = (rotationCount: number): number[] => {
|
||||||
if (rotationCount === 0) return [undefined];
|
if (rotationCount === 0) return [0];
|
||||||
|
|
||||||
const rotationPattern = ["A", "A", "B", "C"];
|
const rotationPattern = [0, 0, 1, 2]; // A, A, B, C
|
||||||
const rotatedValues = [];
|
const rotatedValues = [];
|
||||||
|
|
||||||
for (let i = 0; i < rotationCount; i++) {
|
for (let i = 0; i < rotationCount; i++) {
|
||||||
@ -113,15 +89,15 @@ const getRotations = (rotationCount: number): (string | undefined)[] => {
|
|||||||
return rotatedValues;
|
return rotatedValues;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRandomRewardByChance = (data: IReward[] | undefined): IReward | undefined => {
|
const getRandomRewardByChance = (data: IReward[]): IReward | undefined => {
|
||||||
if (!data || data.length == 0) return;
|
if (data.length == 0) return;
|
||||||
|
|
||||||
const totalChance = data.reduce((sum, item) => sum + item.chance, 0);
|
const totalChance = data.reduce((sum, item) => sum + item.probability!, 0);
|
||||||
const randomValue = Math.random() * totalChance;
|
const randomValue = Math.random() * totalChance;
|
||||||
|
|
||||||
let cumulativeChance = 0;
|
let cumulativeChance = 0;
|
||||||
for (const item of data) {
|
for (const item of data) {
|
||||||
cumulativeChance += item.chance;
|
cumulativeChance += item.probability!;
|
||||||
if (randomValue <= cumulativeChance) {
|
if (randomValue <= cumulativeChance) {
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
@ -130,68 +106,62 @@ const getRandomRewardByChance = (data: IReward[] | undefined): IReward | undefin
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const creditBundles: Record<string, number> = {
|
||||||
|
"/Lotus/StoreItems/Types/PickUps/Credits/1500Credits": 1500,
|
||||||
|
"/Lotus/StoreItems/Types/PickUps/Credits/2000Credits": 2000,
|
||||||
|
"/Lotus/StoreItems/Types/PickUps/Credits/2500Credits": 2500,
|
||||||
|
"/Lotus/StoreItems/Types/PickUps/Credits/3000Credits": 3000,
|
||||||
|
"/Lotus/StoreItems/Types/PickUps/Credits/4000Credits": 4000,
|
||||||
|
"/Lotus/StoreItems/Types/PickUps/Credits/5000Credits": 5000,
|
||||||
|
"/Lotus/StoreItems/Types/PickUps/Credits/7500Credits": 7500,
|
||||||
|
"/Lotus/StoreItems/Types/PickUps/Credits/10000Credits": 10000,
|
||||||
|
"/Lotus/StoreItems/Types/StoreItems/CreditBundles/Zariman/TableACreditsCommon": 15000,
|
||||||
|
"/Lotus/StoreItems/Types/StoreItems/CreditBundles/Zariman/TableACreditsUncommon": 30000,
|
||||||
|
"/Lotus/StoreItems/Types/PickUps/Credits/CorpusArenaCreditRewards/CorpusArenaRewardOneHard": 105000,
|
||||||
|
"/Lotus/StoreItems/Types/PickUps/Credits/CorpusArenaCreditRewards/CorpusArenaRewardTwoHard": 175000,
|
||||||
|
"/Lotus/StoreItems/Types/PickUps/Credits/CorpusArenaCreditRewards/CorpusArenaRewardThreeHard": 25000
|
||||||
|
};
|
||||||
|
|
||||||
|
const fusionBundles: Record<string, number> = {
|
||||||
|
"/Lotus/StoreItems/Upgrades/Mods/FusionBundles/UncommonFusionBundle": 50,
|
||||||
|
"/Lotus/StoreItems/Upgrades/Mods/FusionBundles/RareFusionBundle": 80
|
||||||
|
};
|
||||||
|
|
||||||
const formatRewardsToInventoryType = (
|
const formatRewardsToInventoryType = (
|
||||||
rewards: IReward[]
|
rewards: IReward[]
|
||||||
): { InventoryChanges: IMissionInventoryUpdateRequest; MissionRewards: IMissionRewardResponse[] } => {
|
): { InventoryChanges: IMissionInventoryUpdateRequest; MissionRewards: IMissionRewardResponse[] } => {
|
||||||
const InventoryChanges: IMissionInventoryUpdateRequest = {};
|
const InventoryChanges: IMissionInventoryUpdateRequest = {};
|
||||||
const MissionRewards: IMissionRewardResponse[] = [];
|
const MissionRewards: IMissionRewardResponse[] = [];
|
||||||
for (const reward of rewards) {
|
for (const reward of rewards) {
|
||||||
if (itemCheck(InventoryChanges, MissionRewards, reward.name)) {
|
if (reward.type in creditBundles) {
|
||||||
continue;
|
InventoryChanges.RegularCredits ??= 0;
|
||||||
}
|
InventoryChanges.RegularCredits += creditBundles[reward.type] * reward.itemCount;
|
||||||
|
} else if (reward.type in fusionBundles) {
|
||||||
if (reward.name.includes(" Endo")) {
|
InventoryChanges.FusionPoints ??= 0;
|
||||||
if (!InventoryChanges.FusionPoints) {
|
InventoryChanges.FusionPoints += fusionBundles[reward.type] * reward.itemCount;
|
||||||
InventoryChanges.FusionPoints = 0;
|
} else {
|
||||||
|
const type = reward.type.replace("/Lotus/StoreItems/", "/Lotus/");
|
||||||
|
if (type in ExportUpgrades) {
|
||||||
|
addRewardResponse(InventoryChanges, MissionRewards, type, reward.itemCount, "RawUpgrades");
|
||||||
|
} else if (type in ExportGear) {
|
||||||
|
addRewardResponse(InventoryChanges, MissionRewards, type, reward.itemCount, "Consumables");
|
||||||
|
} else if (type in ExportRecipes) {
|
||||||
|
addRewardResponse(InventoryChanges, MissionRewards, type, reward.itemCount, "Recipes");
|
||||||
|
} else if (type in ExportRelics) {
|
||||||
|
addRewardResponse(InventoryChanges, MissionRewards, type, reward.itemCount, "MiscItems");
|
||||||
|
} else {
|
||||||
|
logger.error(`rolled reward ${reward.itemCount}X ${reward.type} but unsure how to give it`);
|
||||||
}
|
}
|
||||||
InventoryChanges.FusionPoints += getCountFromName(reward.name);
|
|
||||||
} else if (reward.name.includes(" Credits Cache") || reward.name.includes("Return: ")) {
|
|
||||||
if (!InventoryChanges.RegularCredits) {
|
|
||||||
InventoryChanges.RegularCredits = 0;
|
|
||||||
}
|
|
||||||
InventoryChanges.RegularCredits += getCountFromName(reward.name);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { InventoryChanges, MissionRewards };
|
return { InventoryChanges, MissionRewards };
|
||||||
};
|
};
|
||||||
|
|
||||||
const itemCheck = (
|
|
||||||
InventoryChanges: IMissionInventoryUpdateRequest,
|
|
||||||
MissionRewards: IMissionRewardResponse[],
|
|
||||||
name: string
|
|
||||||
) => {
|
|
||||||
const rewardCheck = {
|
|
||||||
RawUpgrades: modNames[name],
|
|
||||||
Consumables: gearNames[name],
|
|
||||||
MiscItems:
|
|
||||||
miscNames[name] ||
|
|
||||||
miscNames[name.replace(/\d+X\s*/, "")] ||
|
|
||||||
resourceNames[name] ||
|
|
||||||
resourceNames[name.replace(/\d+X\s*/, "")] ||
|
|
||||||
relicNames[name.replace("Relic", "Intact")] ||
|
|
||||||
relicNames[name.replace("Relic (Radiant)", "Radiant")],
|
|
||||||
Recipes: blueprintNames[name]
|
|
||||||
};
|
|
||||||
for (const key of Object.keys(rewardCheck) as IInventoryFieldType[]) {
|
|
||||||
if (rewardCheck[key]) {
|
|
||||||
addRewardResponse(InventoryChanges, MissionRewards, name, rewardCheck[key], key);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCountFromName = (name: string) => {
|
|
||||||
const regex = /(^(?:\d{1,3}(?:,\d{3})*(?:\.\d+)?)(\s|X))|(\s(?:\d{1,3}(?:,\d{3})*(?:\.\d+)?)$)/;
|
|
||||||
const countMatches = name.match(regex);
|
|
||||||
return countMatches ? parseInt(countMatches[0].replace(/,/g, ""), 10) : 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
const addRewardResponse = (
|
const addRewardResponse = (
|
||||||
InventoryChanges: IMissionInventoryUpdateRequest,
|
InventoryChanges: IMissionInventoryUpdateRequest,
|
||||||
MissionRewards: IMissionRewardResponse[],
|
MissionRewards: IMissionRewardResponse[],
|
||||||
ItemName: string,
|
|
||||||
ItemType: string,
|
ItemType: string,
|
||||||
|
ItemCount: number,
|
||||||
InventoryCategory: IInventoryFieldType
|
InventoryCategory: IInventoryFieldType
|
||||||
) => {
|
) => {
|
||||||
if (!ItemType) return;
|
if (!ItemType) return;
|
||||||
@ -200,9 +170,6 @@ const addRewardResponse = (
|
|||||||
InventoryChanges[InventoryCategory] = [];
|
InventoryChanges[InventoryCategory] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const ItemCount = getCountFromName(ItemName);
|
|
||||||
const TweetText = `${ItemName}`;
|
|
||||||
|
|
||||||
const existReward = InventoryChanges[InventoryCategory]!.find(item => item.ItemType === ItemType);
|
const existReward = InventoryChanges[InventoryCategory]!.find(item => item.ItemType === ItemType);
|
||||||
if (existReward) {
|
if (existReward) {
|
||||||
existReward.ItemCount += ItemCount;
|
existReward.ItemCount += ItemCount;
|
||||||
@ -214,7 +181,7 @@ const addRewardResponse = (
|
|||||||
InventoryChanges[InventoryCategory]!.push({ ItemType, ItemCount });
|
InventoryChanges[InventoryCategory]!.push({ ItemType, ItemCount });
|
||||||
MissionRewards.push({
|
MissionRewards.push({
|
||||||
ItemCount,
|
ItemCount,
|
||||||
TweetText,
|
TweetText: ItemType, // ensure if/how this even still used, or if it's needed at all
|
||||||
ProductCategory: InventoryCategory,
|
ProductCategory: InventoryCategory,
|
||||||
StoreItem: ItemType.replace("/Lotus/", "/Lotus/StoreItems/"),
|
StoreItem: ItemType.replace("/Lotus/", "/Lotus/StoreItems/"),
|
||||||
TypeName: ItemType
|
TypeName: ItemType
|
||||||
@ -222,32 +189,4 @@ const addRewardResponse = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const _missionRewardsCheckAllNamings = () => {
|
|
||||||
let tempRewards: IReward[] = [];
|
|
||||||
Object.values(missionsDropTable as { [key: string]: IReward[] }).forEach(rewards => {
|
|
||||||
rewards.forEach(reward => {
|
|
||||||
tempRewards.push(reward);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
tempRewards = tempRewards
|
|
||||||
.filter(reward => !modNames[reward.name])
|
|
||||||
.filter(reward => !miscNames[reward.name])
|
|
||||||
.filter(reward => !miscNames[reward.name.replace(/\d+X\s*/, "")])
|
|
||||||
.filter(reward => !resourceNames[reward.name])
|
|
||||||
.filter(reward => !resourceNames[reward.name.replace(/\d+X\s*/, "")])
|
|
||||||
.filter(reward => !gearNames[reward.name])
|
|
||||||
.filter(reward => {
|
|
||||||
return (
|
|
||||||
!relicNames[reward.name.replace("Relic", "Intact")] &&
|
|
||||||
!relicNames[reward.name.replace("Relic (Radiant)", "Radiant")]
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.filter(reward => !blueprintNames[reward.name])
|
|
||||||
.filter(reward => !reward.name.includes(" Endo"))
|
|
||||||
.filter(reward => !reward.name.includes(" Credits Cache") && !reward.name.includes("Return: "));
|
|
||||||
logger.debug(`temp rewards`, { tempRewards });
|
|
||||||
};
|
|
||||||
// _missionRewardsCheckAllNamings();
|
|
||||||
|
|
||||||
export { getRewards, combineRewardAndLootInventory };
|
export { getRewards, combineRewardAndLootInventory };
|
||||||
|
@ -9,9 +9,3 @@ export interface IMissionRewardResponse {
|
|||||||
TweetText: string;
|
TweetText: string;
|
||||||
ProductCategory: string;
|
ProductCategory: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IReward {
|
|
||||||
name: string;
|
|
||||||
chance: number;
|
|
||||||
rotation?: string;
|
|
||||||
}
|
|
||||||
|
@ -67,7 +67,7 @@ export interface IMissionInventoryUpdateRequestRewardInfo {
|
|||||||
lostTargetWave?: number;
|
lostTargetWave?: number;
|
||||||
defenseTargetCount?: number;
|
defenseTargetCount?: number;
|
||||||
EOM_AFK?: number;
|
EOM_AFK?: number;
|
||||||
rewardQualifications?: string;
|
rewardQualifications?: string; // did a Survival for 5 minutes and this was "1"
|
||||||
PurgatoryRewardQualifications?: string;
|
PurgatoryRewardQualifications?: string;
|
||||||
rewardSeed?: number;
|
rewardSeed?: number;
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user