mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-22 17:10:10 +00:00
Discord: Fix GetUsers, add invite messages
The "Send to" box on the invite tab now accepts username#discriminator, and a search icon has been added which opens a search window similar to the one on the accounts tab. DiscordDaemon.GetUsers was also very broken and wouldn't work with full username#discriminator, that's been fixed.
This commit is contained in:
parent
b8e3fc636c
commit
ce8cdced4d
48
api.go
48
api.go
@ -826,18 +826,44 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
|
|||||||
invite.UserMinutes = req.UserMinutes
|
invite.UserMinutes = req.UserMinutes
|
||||||
}
|
}
|
||||||
invite.ValidTill = validTill
|
invite.ValidTill = validTill
|
||||||
if emailEnabled && req.Email != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) {
|
if req.SendTo != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) {
|
||||||
app.debug.Printf("%s: Sending invite email", inviteCode)
|
addressValid := false
|
||||||
invite.Email = req.Email
|
discord := ""
|
||||||
|
app.debug.Printf("%s: Sending invite message", inviteCode)
|
||||||
|
if discordEnabled && !strings.Contains(req.SendTo, "@") {
|
||||||
|
users := app.discord.GetUsers(req.SendTo)
|
||||||
|
if len(users) == 0 {
|
||||||
|
invite.SendTo = fmt.Sprintf("Failed: User not found: \"%s\"", req.SendTo)
|
||||||
|
} else if len(users) > 1 {
|
||||||
|
invite.SendTo = fmt.Sprintf("Failed: Multiple users found: \"%s\"", req.SendTo)
|
||||||
|
} else {
|
||||||
|
invite.SendTo = req.SendTo
|
||||||
|
addressValid = true
|
||||||
|
discord = users[0].User.ID
|
||||||
|
}
|
||||||
|
} else if emailEnabled {
|
||||||
|
addressValid = true
|
||||||
|
invite.SendTo = req.SendTo
|
||||||
|
}
|
||||||
|
if addressValid {
|
||||||
msg, err := app.email.constructInvite(inviteCode, invite, app, false)
|
msg, err := app.email.constructInvite(inviteCode, invite, app, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
invite.Email = fmt.Sprintf("Failed to send to %s", req.Email)
|
invite.SendTo = fmt.Sprintf("Failed to send to %s", req.SendTo)
|
||||||
app.err.Printf("%s: Failed to construct invite email: %v", inviteCode, err)
|
app.err.Printf("%s: Failed to construct invite message: %v", inviteCode, err)
|
||||||
} else if err := app.email.send(msg, req.Email); err != nil {
|
|
||||||
invite.Email = fmt.Sprintf("Failed to send to %s", req.Email)
|
|
||||||
app.err.Printf("%s: %s: %v", inviteCode, invite.Email, err)
|
|
||||||
} else {
|
} else {
|
||||||
app.info.Printf("%s: Sent invite email to \"%s\"", inviteCode, req.Email)
|
var err error
|
||||||
|
if discord != "" {
|
||||||
|
err = app.discord.SendDM(msg, discord)
|
||||||
|
} else {
|
||||||
|
err = app.email.send(msg, req.SendTo)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
invite.SendTo = fmt.Sprintf("Failed to send to %s", req.SendTo)
|
||||||
|
app.err.Printf("%s: %s: %v", inviteCode, invite.SendTo, err)
|
||||||
|
} else {
|
||||||
|
app.info.Printf("%s: Sent invite email to \"%s\"", inviteCode, req.SendTo)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if req.Profile != "" {
|
if req.Profile != "" {
|
||||||
@ -901,8 +927,8 @@ func (app *appContext) GetInvites(gc *gin.Context) {
|
|||||||
if inv.RemainingUses != 0 {
|
if inv.RemainingUses != 0 {
|
||||||
invite.RemainingUses = inv.RemainingUses
|
invite.RemainingUses = inv.RemainingUses
|
||||||
}
|
}
|
||||||
if inv.Email != "" {
|
if inv.SendTo != "" {
|
||||||
invite.Email = inv.Email
|
invite.SendTo = inv.SendTo
|
||||||
}
|
}
|
||||||
if len(inv.Notify) != 0 {
|
if len(inv.Notify) != 0 {
|
||||||
var address string
|
var address string
|
||||||
|
24
discord.go
24
discord.go
@ -111,13 +111,13 @@ func (d *DiscordDaemon) GetUsers(username string) []*dg.Member {
|
|||||||
hasDiscriminator := strings.Contains(username, "#")
|
hasDiscriminator := strings.Contains(username, "#")
|
||||||
var users []*dg.Member
|
var users []*dg.Member
|
||||||
for _, member := range members {
|
for _, member := range members {
|
||||||
if !hasDiscriminator {
|
if hasDiscriminator {
|
||||||
userSplit := strings.Split(member.User.Username, "#")
|
if member.User.Username+"#"+member.User.Discriminator == username {
|
||||||
if strings.Contains(userSplit[0], username) {
|
return []*dg.Member{member}
|
||||||
users = append(users, member)
|
|
||||||
}
|
}
|
||||||
} else if strings.Contains(member.User.Username, username) {
|
}
|
||||||
return nil
|
if strings.Contains(member.User.Username, username) {
|
||||||
|
users = append(users, member)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return users
|
return users
|
||||||
@ -283,6 +283,18 @@ func (d *DiscordDaemon) commandPIN(s *dg.Session, m *dg.MessageCreate, sects []s
|
|||||||
d.tokens = d.tokens[:len(d.tokens)-1]
|
d.tokens = d.tokens[:len(d.tokens)-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *DiscordDaemon) SendDM(message *Message, userID ...string) error {
|
||||||
|
channels := make([]string, len(userID))
|
||||||
|
for i, id := range userID {
|
||||||
|
channel, err := d.bot.UserChannelCreate(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
channels[i] = channel.ID
|
||||||
|
}
|
||||||
|
return d.Send(message, channels...)
|
||||||
|
}
|
||||||
|
|
||||||
func (d *DiscordDaemon) Send(message *Message, channelID ...string) error {
|
func (d *DiscordDaemon) Send(message *Message, channelID ...string) error {
|
||||||
msg := ""
|
msg := ""
|
||||||
var embeds []*dg.MessageEmbed
|
var embeds []*dg.MessageEmbed
|
||||||
|
@ -501,7 +501,14 @@
|
|||||||
<div id="create-send-to-container">
|
<div id="create-send-to-container">
|
||||||
<label class="label supra">{{ .strings.inviteSendToEmail }}</label>
|
<label class="label supra">{{ .strings.inviteSendToEmail }}</label>
|
||||||
<div class="flex-expand mb-1 mt-half">
|
<div class="flex-expand mb-1 mt-half">
|
||||||
|
{{ if .discord_enabled }}
|
||||||
|
<input type="text" id="create-send-to" class="input ~neutral !normal mr-1" placeholder="example@example.com | user#1234">
|
||||||
|
<span id="create-send-to-search" class="button ~neutral !normal mr-1">
|
||||||
|
<i class="icon ri-search-2-line" title="{{ .strings.search }}"></i>
|
||||||
|
</span>
|
||||||
|
{{ else }}
|
||||||
<input type="email" id="create-send-to" class="input ~neutral !normal mr-1" placeholder="example@example.com">
|
<input type="email" id="create-send-to" class="input ~neutral !normal mr-1" placeholder="example@example.com">
|
||||||
|
{{ end }}
|
||||||
<label for="create-send-to-enabled" class="button ~neutral !normal">
|
<label for="create-send-to-enabled" class="button ~neutral !normal">
|
||||||
<input type="checkbox" id="create-send-to-enabled" aria-label="Send to address enabled">
|
<input type="checkbox" id="create-send-to-enabled" aria-label="Send to address enabled">
|
||||||
</label>
|
</label>
|
||||||
|
@ -97,7 +97,8 @@
|
|||||||
"notifyInviteExpiry": "On expiry",
|
"notifyInviteExpiry": "On expiry",
|
||||||
"notifyUserCreation": "On user creation",
|
"notifyUserCreation": "On user creation",
|
||||||
"sendPIN": "Ask the user to send the PIN below to the bot.",
|
"sendPIN": "Ask the user to send the PIN below to the bot.",
|
||||||
"searchDiscordUser": "Start typing the Discord username to link it."
|
"searchDiscordUser": "Start typing the Discord username to find the user.",
|
||||||
|
"findDiscordUser": "Find Discord user"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"changedEmailAddress": "Changed email address of {n}.",
|
"changedEmailAddress": "Changed email address of {n}.",
|
||||||
|
@ -50,7 +50,7 @@ type generateInviteDTO struct {
|
|||||||
UserDays int `json:"user-days,omitempty" example:"1"` // Number of days till user expiry
|
UserDays int `json:"user-days,omitempty" example:"1"` // Number of days till user expiry
|
||||||
UserHours int `json:"user-hours,omitempty" example:"2"` // Number of hours till user expiry
|
UserHours int `json:"user-hours,omitempty" example:"2"` // Number of hours till user expiry
|
||||||
UserMinutes int `json:"user-minutes,omitempty" example:"3"` // Number of minutes till user expiry
|
UserMinutes int `json:"user-minutes,omitempty" example:"3"` // Number of minutes till user expiry
|
||||||
Email string `json:"email" example:"jeff@jellyf.in"` // Send invite to this address
|
SendTo string `json:"send-to" example:"jeff@jellyf.in"` // Send invite to this address or discord name
|
||||||
MultipleUses bool `json:"multiple-uses" example:"true"` // Allow multiple uses
|
MultipleUses bool `json:"multiple-uses" example:"true"` // Allow multiple uses
|
||||||
NoLimit bool `json:"no-limit" example:"false"` // No invite use limit
|
NoLimit bool `json:"no-limit" example:"false"` // No invite use limit
|
||||||
RemainingUses int `json:"remaining-uses" example:"5"` // Remaining invite uses
|
RemainingUses int `json:"remaining-uses" example:"5"` // Remaining invite uses
|
||||||
@ -100,7 +100,7 @@ type inviteDTO struct {
|
|||||||
UsedBy map[string]int64 `json:"used-by,omitempty"` // Users who have used this invite mapped to their creation time in Epoch/Unix time
|
UsedBy map[string]int64 `json:"used-by,omitempty"` // Users who have used this invite mapped to their creation time in Epoch/Unix time
|
||||||
NoLimit bool `json:"no-limit,omitempty"` // If true, invite can be used any number of times
|
NoLimit bool `json:"no-limit,omitempty"` // If true, invite can be used any number of times
|
||||||
RemainingUses int `json:"remaining-uses,omitempty"` // Remaining number of uses (if applicable)
|
RemainingUses int `json:"remaining-uses,omitempty"` // Remaining number of uses (if applicable)
|
||||||
Email string `json:"email,omitempty"` // Email the invite was sent to (if applicable)
|
SendTo string `json:"send_to,omitempty"` // Email/Discord username the invite was sent to (if applicable)
|
||||||
NotifyExpiry bool `json:"notify-expiry,omitempty"` // Whether to notify the requesting user of expiry or not
|
NotifyExpiry bool `json:"notify-expiry,omitempty"` // Whether to notify the requesting user of expiry or not
|
||||||
NotifyCreation bool `json:"notify-creation,omitempty"` // Whether to notify the requesting user of account creation or not
|
NotifyCreation bool `json:"notify-creation,omitempty"` // Whether to notify the requesting user of account creation or not
|
||||||
Label string `json:"label,omitempty" example:"For Friends"` // Optional label for the invite
|
Label string `json:"label,omitempty" example:"For Friends"` // Optional label for the invite
|
||||||
|
@ -95,7 +95,7 @@ type Invite struct {
|
|||||||
UserDays int `json:"user-days,omitempty"`
|
UserDays int `json:"user-days,omitempty"`
|
||||||
UserHours int `json:"user-hours,omitempty"`
|
UserHours int `json:"user-hours,omitempty"`
|
||||||
UserMinutes int `json:"user-minutes,omitempty"`
|
UserMinutes int `json:"user-minutes,omitempty"`
|
||||||
Email string `json:"email"`
|
SendTo string `json:"email"`
|
||||||
// Used to be stored as formatted time, now as Unix.
|
// Used to be stored as formatted time, now as Unix.
|
||||||
UsedBy [][]string `json:"used-by"`
|
UsedBy [][]string `json:"used-by"`
|
||||||
Notify map[string]map[string]bool `json:"notify"`
|
Notify map[string]map[string]bool `json:"notify"`
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { _get, _post, _delete, toClipboard, toggleLoader, toDateString } from "../modules/common.js";
|
import { _get, _post, _delete, toClipboard, toggleLoader, toDateString } from "../modules/common.js";
|
||||||
import { newDiscordSearch } from "../modules/discord.js";
|
import { DiscordUser, newDiscordSearch } from "../modules/discord.js";
|
||||||
|
|
||||||
class DOMInvite implements Invite {
|
class DOMInvite implements Invite {
|
||||||
updateNotify = (checkbox: HTMLInputElement) => {
|
updateNotify = (checkbox: HTMLInputElement) => {
|
||||||
@ -26,6 +26,7 @@ class DOMInvite implements Invite {
|
|||||||
document.dispatchEvent(inviteDeletedEvent);
|
document.dispatchEvent(inviteDeletedEvent);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
private _label: string = "";
|
private _label: string = "";
|
||||||
get label(): string { return this._label; }
|
get label(): string { return this._label; }
|
||||||
set label(label: string) {
|
set label(label: string) {
|
||||||
@ -83,10 +84,10 @@ class DOMInvite implements Invite {
|
|||||||
this._middle.querySelector("strong.inv-remaining").textContent = remaining;
|
this._middle.querySelector("strong.inv-remaining").textContent = remaining;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _email: string = "";
|
private _send_to: string = "";
|
||||||
get email(): string { return this._email };
|
get send_to(): string { return this._send_to };
|
||||||
set email(address: string) {
|
set send_to(address: string) {
|
||||||
this._email = address;
|
this._send_to = address;
|
||||||
const container = this._infoArea.querySelector(".tooltip") as HTMLDivElement;
|
const container = this._infoArea.querySelector(".tooltip") as HTMLDivElement;
|
||||||
const icon = container.querySelector("i");
|
const icon = container.querySelector("i");
|
||||||
const chip = container.querySelector("span.inv-email-chip");
|
const chip = container.querySelector("span.inv-email-chip");
|
||||||
@ -101,7 +102,7 @@ class DOMInvite implements Invite {
|
|||||||
} else {
|
} else {
|
||||||
container.classList.add("mr-1");
|
container.classList.add("mr-1");
|
||||||
chip.classList.add("chip");
|
chip.classList.add("chip");
|
||||||
if (address.includes("Failed to send to")) {
|
if (address.includes("Failed")) {
|
||||||
icon.classList.remove("ri-mail-line");
|
icon.classList.remove("ri-mail-line");
|
||||||
icon.classList.add("ri-mail-close-line");
|
icon.classList.add("ri-mail-close-line");
|
||||||
chip.classList.remove("~neutral");
|
chip.classList.remove("~neutral");
|
||||||
@ -373,7 +374,7 @@ class DOMInvite implements Invite {
|
|||||||
update = (invite: Invite) => {
|
update = (invite: Invite) => {
|
||||||
this.code = invite.code;
|
this.code = invite.code;
|
||||||
this.created = invite.created;
|
this.created = invite.created;
|
||||||
this.email = invite.email;
|
this.send_to = invite.send_to;
|
||||||
this.expiresIn = invite.expiresIn;
|
this.expiresIn = invite.expiresIn;
|
||||||
if (window.notificationsEnabled) {
|
if (window.notificationsEnabled) {
|
||||||
this.notifyCreation = invite.notifyCreation;
|
this.notifyCreation = invite.notifyCreation;
|
||||||
@ -483,7 +484,7 @@ export class inviteList implements inviteList {
|
|||||||
function parseInvite(invite: { [f: string]: string | number | { [name: string]: number } | boolean }): Invite {
|
function parseInvite(invite: { [f: string]: string | number | { [name: string]: number } | boolean }): Invite {
|
||||||
let parsed: Invite = {};
|
let parsed: Invite = {};
|
||||||
parsed.code = invite["code"] as string;
|
parsed.code = invite["code"] as string;
|
||||||
parsed.email = invite["email"] as string || "";
|
parsed.send_to = invite["send_to"] as string || "";
|
||||||
parsed.label = invite["label"] as string || "";
|
parsed.label = invite["label"] as string || "";
|
||||||
let time = "";
|
let time = "";
|
||||||
let userExpiryTime = "";
|
let userExpiryTime = "";
|
||||||
@ -521,6 +522,7 @@ function parseInvite(invite: { [f: string]: string | number | { [name: string]:
|
|||||||
export class createInvite {
|
export class createInvite {
|
||||||
private _sendToEnabled = document.getElementById("create-send-to-enabled") as HTMLInputElement;
|
private _sendToEnabled = document.getElementById("create-send-to-enabled") as HTMLInputElement;
|
||||||
private _sendTo = document.getElementById("create-send-to") as HTMLInputElement;
|
private _sendTo = document.getElementById("create-send-to") as HTMLInputElement;
|
||||||
|
private _discordSearch: HTMLSpanElement;
|
||||||
private _userExpiryToggle = document.getElementById("create-user-expiry-enabled") as HTMLInputElement;
|
private _userExpiryToggle = document.getElementById("create-user-expiry-enabled") as HTMLInputElement;
|
||||||
private _uses = document.getElementById('create-uses') as HTMLInputElement;
|
private _uses = document.getElementById('create-uses') as HTMLInputElement;
|
||||||
private _infUses = document.getElementById("create-inf-uses") as HTMLInputElement;
|
private _infUses = document.getElementById("create-inf-uses") as HTMLInputElement;
|
||||||
@ -543,6 +545,8 @@ export class createInvite {
|
|||||||
private _invDuration = document.getElementById('inv-duration');
|
private _invDuration = document.getElementById('inv-duration');
|
||||||
private _userExpiry = document.getElementById('user-expiry');
|
private _userExpiry = document.getElementById('user-expiry');
|
||||||
|
|
||||||
|
private _sendToDiscord: (passData: string) => void;
|
||||||
|
|
||||||
// Broadcast when new invite created
|
// Broadcast when new invite created
|
||||||
private _newInviteEvent = new CustomEvent("newInviteEvent");
|
private _newInviteEvent = new CustomEvent("newInviteEvent");
|
||||||
private _firstLoad = true;
|
private _firstLoad = true;
|
||||||
@ -577,9 +581,19 @@ export class createInvite {
|
|||||||
if (state) {
|
if (state) {
|
||||||
this._sendToEnabled.parentElement.classList.remove("~neutral");
|
this._sendToEnabled.parentElement.classList.remove("~neutral");
|
||||||
this._sendToEnabled.parentElement.classList.add("~urge");
|
this._sendToEnabled.parentElement.classList.add("~urge");
|
||||||
|
if (window.discordEnabled) {
|
||||||
|
this._discordSearch.classList.remove("~neutral");
|
||||||
|
this._discordSearch.classList.add("~urge");
|
||||||
|
this._discordSearch.onclick = () => this._sendToDiscord("");
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this._sendToEnabled.parentElement.classList.remove("~urge");
|
this._sendToEnabled.parentElement.classList.remove("~urge");
|
||||||
this._sendToEnabled.parentElement.classList.add("~neutral");
|
this._sendToEnabled.parentElement.classList.add("~neutral");
|
||||||
|
if (window.discordEnabled) {
|
||||||
|
this._discordSearch.classList.remove("~urge");
|
||||||
|
this._discordSearch.classList.add("~neutral");
|
||||||
|
this._discordSearch.onclick = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -733,7 +747,7 @@ export class createInvite {
|
|||||||
"multiple-uses": (this.uses > 1 || this.infiniteUses),
|
"multiple-uses": (this.uses > 1 || this.infiniteUses),
|
||||||
"no-limit": this.infiniteUses,
|
"no-limit": this.infiniteUses,
|
||||||
"remaining-uses": this.uses,
|
"remaining-uses": this.uses,
|
||||||
"email": this.sendToEnabled ? this.sendTo : "",
|
"send-to": this.sendToEnabled ? this.sendTo : "",
|
||||||
"profile": this.profile,
|
"profile": this.profile,
|
||||||
"label": this.label
|
"label": this.label
|
||||||
};
|
};
|
||||||
@ -762,7 +776,6 @@ export class createInvite {
|
|||||||
this._userDays.disabled = true;
|
this._userDays.disabled = true;
|
||||||
this._userHours.disabled = true;
|
this._userHours.disabled = true;
|
||||||
this._userMinutes.disabled = true;
|
this._userMinutes.disabled = true;
|
||||||
this.sendToEnabled = false;
|
|
||||||
this._createButton.onclick = this.create;
|
this._createButton.onclick = this.create;
|
||||||
this.sendTo = "";
|
this.sendTo = "";
|
||||||
this.uses = 1;
|
this.uses = 1;
|
||||||
@ -799,11 +812,22 @@ export class createInvite {
|
|||||||
this._minutes.onchange = this._checkDurationValidity;
|
this._minutes.onchange = this._checkDurationValidity;
|
||||||
document.addEventListener("profileLoadEvent", () => { this.loadProfiles(); }, false);
|
document.addEventListener("profileLoadEvent", () => { this.loadProfiles(); }, false);
|
||||||
|
|
||||||
if (!window.emailEnabled) {
|
if (!window.emailEnabled && !window.discordEnabled) {
|
||||||
document.getElementById("create-send-to-container").classList.add("unfocused");
|
document.getElementById("create-send-to-container").classList.add("unfocused");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (window.discordEnabled) {
|
||||||
|
this._discordSearch = document.getElementById("create-send-to-search") as HTMLSpanElement;
|
||||||
|
this._sendToDiscord = newDiscordSearch(
|
||||||
|
window.lang.strings("findDiscordUser"),
|
||||||
|
window.lang.strings("searchDiscordUser"),
|
||||||
|
window.lang.strings("select"),
|
||||||
|
(user: DiscordUser) => {
|
||||||
|
this.sendTo = user.name;
|
||||||
|
window.modals.discord.close();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.sendToEnabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -109,7 +109,7 @@ interface Invite {
|
|||||||
code?: string;
|
code?: string;
|
||||||
expiresIn?: string;
|
expiresIn?: string;
|
||||||
remainingUses?: string;
|
remainingUses?: string;
|
||||||
email?: string;
|
send_to?: string;
|
||||||
usedBy?: { [name: string]: number };
|
usedBy?: { [name: string]: number };
|
||||||
created?: number;
|
created?: number;
|
||||||
notifyExpiry?: boolean;
|
notifyExpiry?: boolean;
|
||||||
|
4
views.go
4
views.go
@ -258,8 +258,8 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
email := app.storage.invites[code].Email
|
email := app.storage.invites[code].SendTo
|
||||||
if strings.Contains(email, "Failed") {
|
if strings.Contains(email, "Failed") || !strings.Contains(email, "@") {
|
||||||
email = ""
|
email = ""
|
||||||
}
|
}
|
||||||
data := gin.H{
|
data := gin.H{
|
||||||
|
Loading…
Reference in New Issue
Block a user