mirror of
https://github.com/samuelthomas2774/nxapi.git
synced 2026-09-07 10:55:09 -05:00
Rename most instances of znc to coral
This commit is contained in:
@@ -1,16 +1,16 @@
|
||||
|
||||
export interface ZncSuccessResponse<T = unknown> {
|
||||
status: ZncStatus.OK;
|
||||
export interface CoralSuccessResponse<T = unknown> {
|
||||
status: CoralStatus.OK;
|
||||
result: T;
|
||||
correlationId: string;
|
||||
}
|
||||
|
||||
export interface ZncErrorResponse {
|
||||
status: ZncStatus | number;
|
||||
export interface CoralErrorResponse {
|
||||
status: CoralStatus | number;
|
||||
errorMessage: string;
|
||||
correlationId: string;
|
||||
}
|
||||
export enum ZncStatus {
|
||||
export enum CoralStatus {
|
||||
OK = 0,
|
||||
|
||||
BAD_REQUEST = 9400,
|
||||
@@ -43,7 +43,7 @@ export enum ZncStatus {
|
||||
// UNKNOWN = -1,
|
||||
}
|
||||
|
||||
export type ZncResponse<T = unknown> = ZncSuccessResponse<T> | ZncErrorResponse;
|
||||
export type CoralResponse<T = unknown> = CoralSuccessResponse<T> | CoralErrorResponse;
|
||||
|
||||
/** /v3/Account/Login */
|
||||
export interface AccountLogin {
|
||||
@@ -2,12 +2,12 @@ import fetch, { Response } from 'node-fetch';
|
||||
import { v4 as uuidgen } from 'uuid';
|
||||
import createDebug from 'debug';
|
||||
import { f, FlapgIid } from './f.js';
|
||||
import { AccountLogin, AccountToken, Announcements, CurrentUser, CurrentUserPermissions, Event, Friends, GetActiveEventResult, PresencePermissions, User, WebServices, WebServiceToken, ZncErrorResponse, ZncResponse, ZncStatus, ZncSuccessResponse } from './znc-types.js';
|
||||
import { AccountLogin, AccountToken, Announcements, CurrentUser, CurrentUserPermissions, Event, Friends, GetActiveEventResult, PresencePermissions, User, WebServices, WebServiceToken, CoralErrorResponse, CoralResponse, CoralStatus, CoralSuccessResponse } from './coral-types.js';
|
||||
import { getNintendoAccountToken, getNintendoAccountUser, NintendoAccountUser } from './na.js';
|
||||
import { ErrorResponse } from './util.js';
|
||||
import { JwtPayload } from '../util/jwt.js';
|
||||
|
||||
const debug = createDebug('nxapi:api:znc');
|
||||
const debug = createDebug('nxapi:api:coral');
|
||||
|
||||
const ZNCA_PLATFORM = 'Android';
|
||||
const ZNCA_PLATFORM_VERSION = '8.0.0';
|
||||
@@ -17,23 +17,23 @@ const ZNCA_USER_AGENT = `com.nintendo.znca/${ZNCA_VERSION}(${ZNCA_PLATFORM}/${ZN
|
||||
const ZNC_URL = 'https://api-lp1.znc.srv.nintendo.net';
|
||||
export const ZNCA_CLIENT_ID = '71b963c1b7b6d119';
|
||||
|
||||
export default class ZncApi {
|
||||
export default class CoralApi {
|
||||
static useragent: string | null = null;
|
||||
|
||||
onTokenExpired: ((data: ZncErrorResponse, res: Response) => Promise<void>) | null = null;
|
||||
onTokenExpired: ((data: CoralErrorResponse, res: Response) => Promise<void>) | null = null;
|
||||
/** @internal */
|
||||
_renewToken: Promise<void> | null = null;
|
||||
|
||||
constructor(
|
||||
public token: string,
|
||||
public useragent: string | null = ZncApi.useragent
|
||||
public useragent: string | null = CoralApi.useragent
|
||||
) {}
|
||||
|
||||
async fetch<T = unknown>(
|
||||
url: string, method = 'GET', body?: string, headers?: object,
|
||||
/** @internal */ _autoRenewToken = true,
|
||||
/** @internal */ _attempt = 0
|
||||
): Promise<ZncSuccessResponse<T>> {
|
||||
): Promise<CoralSuccessResponse<T>> {
|
||||
if (this._renewToken && _autoRenewToken) {
|
||||
await this._renewToken;
|
||||
}
|
||||
@@ -52,9 +52,9 @@ export default class ZncApi {
|
||||
|
||||
debug('fetch %s %s, response %s', method, url, response.status);
|
||||
|
||||
const data = await response.json() as ZncResponse<T>;
|
||||
const data = await response.json() as CoralResponse<T>;
|
||||
|
||||
if (data.status === ZncStatus.TOKEN_EXPIRED && _autoRenewToken && !_attempt && this.onTokenExpired) {
|
||||
if (data.status === CoralStatus.TOKEN_EXPIRED && _autoRenewToken && !_attempt && this.onTokenExpired) {
|
||||
// _renewToken will be awaited when calling fetch
|
||||
this._renewToken = this._renewToken ?? this.onTokenExpired.call(null, data, response).finally(() => {
|
||||
this._renewToken = null;
|
||||
@@ -65,7 +65,7 @@ export default class ZncApi {
|
||||
if ('errorMessage' in data) {
|
||||
throw new ErrorResponse('[znc] ' + data.errorMessage, response, data);
|
||||
}
|
||||
if (data.status !== ZncStatus.OK) {
|
||||
if (data.status !== CoralStatus.OK) {
|
||||
throw new ErrorResponse('[znc] Unknown error', response, data);
|
||||
}
|
||||
|
||||
@@ -194,7 +194,7 @@ export default class ZncApi {
|
||||
};
|
||||
}
|
||||
|
||||
static async createWithSessionToken(token: string, useragent = ZncApi.useragent) {
|
||||
static async createWithSessionToken(token: string, useragent = CoralApi.useragent) {
|
||||
const data = await this.loginWithSessionToken(token, useragent);
|
||||
|
||||
return {
|
||||
@@ -211,7 +211,7 @@ export default class ZncApi {
|
||||
return data;
|
||||
}
|
||||
|
||||
static async loginWithSessionToken(token: string, useragent = ZncApi.useragent) {
|
||||
static async loginWithSessionToken(token: string, useragent = CoralApi.useragent) {
|
||||
const uuid = uuidgen();
|
||||
const timestamp = '' + Math.floor(Date.now() / 1000);
|
||||
|
||||
@@ -248,7 +248,7 @@ export default class ZncApi {
|
||||
});
|
||||
|
||||
debug('fetch %s %s, response %s', 'POST', '/v3/Account/Login', response.status);
|
||||
const data = await response.json() as ZncResponse<AccountLogin>;
|
||||
const data = await response.json() as CoralResponse<AccountLogin>;
|
||||
|
||||
if ('errorMessage' in data) {
|
||||
throw new ErrorResponse('[znc] ' + data.errorMessage, response, data);
|
||||
@@ -271,7 +271,7 @@ export default class ZncApi {
|
||||
}
|
||||
}
|
||||
|
||||
export interface ZncJwtPayload extends JwtPayload {
|
||||
export interface CoralJwtPayload extends JwtPayload {
|
||||
isChildRestricted: boolean;
|
||||
membership: {
|
||||
active: boolean;
|
||||
@@ -280,18 +280,18 @@ export interface ZncJwtPayload extends JwtPayload {
|
||||
exp: number;
|
||||
iat: number;
|
||||
iss: 'api-lp1.znc.srv.nintendo.net';
|
||||
/** User ID (CurrentUser.id, not CurrentUser.nsaID) */
|
||||
/** Coral user ID (CurrentUser.id, not CurrentUser.nsaID) */
|
||||
sub: number;
|
||||
typ: 'id_token';
|
||||
}
|
||||
export interface ZncWebServiceJwtPayload extends JwtPayload {
|
||||
export interface CoralWebServiceJwtPayload extends JwtPayload {
|
||||
isChildRestricted: boolean;
|
||||
aud: string;
|
||||
exp: number;
|
||||
iat: number;
|
||||
iss: 'api-lp1.znc.srv.nintendo.net';
|
||||
jti: string;
|
||||
/** User ID (CurrentUser.id, not CurrentUser.nsaID) */
|
||||
/** Coral user ID (CurrentUser.id, not CurrentUser.nsaID) */
|
||||
sub: number;
|
||||
links: {
|
||||
networkServiceAccount: {
|
||||
103
src/api/f.ts
103
src/api/f.ts
@@ -10,6 +10,14 @@ const debugFlapg = createDebug('nxapi:api:flapg');
|
||||
const debugImink = createDebug('nxapi:api:imink');
|
||||
const debugZncaApi = createDebug('nxapi:api:znca-api');
|
||||
|
||||
abstract class ZncaApi {
|
||||
constructor(
|
||||
public useragent?: string
|
||||
) {}
|
||||
|
||||
abstract genf(token: string, timestamp: string, uuid: string, type: FlapgIid): Promise<FResult>;
|
||||
}
|
||||
|
||||
//
|
||||
// splatnet2statink + flapg
|
||||
//
|
||||
@@ -96,6 +104,23 @@ export interface FlapgApiResponse {
|
||||
};
|
||||
}
|
||||
|
||||
export class ZncaApiFlapg extends ZncaApi {
|
||||
async getLoginHash(id_token: string, timestamp: string) {
|
||||
return getLoginHash(id_token, timestamp, this.useragent);
|
||||
}
|
||||
|
||||
async genf(token: string, timestamp: string, uuid: string, type: FlapgIid) {
|
||||
const result = await flapg(token, timestamp, uuid, type, this.useragent);
|
||||
|
||||
return {
|
||||
provider: 'flapg' as const,
|
||||
token, timestamp, uuid, type,
|
||||
f: result.result.f,
|
||||
result,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// imink
|
||||
//
|
||||
@@ -151,6 +176,19 @@ export interface IminkFError {
|
||||
error: true;
|
||||
}
|
||||
|
||||
export class ZncaApiImink extends ZncaApi {
|
||||
async genf(token: string, timestamp: string, uuid: string, type: FlapgIid) {
|
||||
const result = await iminkf(token, timestamp, uuid, type === FlapgIid.APP ? '2' : '1', this.useragent);
|
||||
|
||||
return {
|
||||
provider: 'imink' as const,
|
||||
token, timestamp, uuid, type,
|
||||
f: result.f,
|
||||
result,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// nxapi znca API server
|
||||
//
|
||||
@@ -206,43 +244,32 @@ export interface AndroidZncaFError {
|
||||
error: string;
|
||||
}
|
||||
|
||||
export async function f(
|
||||
token: string, timestamp: string | number, uuid: string, type: FlapgIid,
|
||||
useragent?: string,
|
||||
provider: FApi = getEnvApi()
|
||||
): Promise<FResult> {
|
||||
if (provider === 'flapg') {
|
||||
const result = await flapg(token, timestamp, uuid, type, useragent);
|
||||
return {
|
||||
provider: 'flapg',
|
||||
token, timestamp: '' + timestamp, uuid, type,
|
||||
f: result.result.f, result,
|
||||
};
|
||||
}
|
||||
if (provider === 'imink') {
|
||||
const result = await iminkf(token, timestamp, uuid, type === FlapgIid.APP ? '2' : '1', useragent);
|
||||
return {
|
||||
provider: 'imink',
|
||||
token, timestamp: '' + timestamp, uuid, type,
|
||||
f: result.f, result,
|
||||
};
|
||||
}
|
||||
if (provider[0] === 'nxapi') {
|
||||
const result = await genf(provider[1], token, timestamp, uuid, type, useragent);
|
||||
return {
|
||||
provider: 'nxapi', url: provider[1],
|
||||
token, timestamp: '' + timestamp, uuid, type,
|
||||
f: result.f, result,
|
||||
};
|
||||
export class ZncaApiNxapi extends ZncaApi {
|
||||
constructor(readonly url: string, useragent?: string) {
|
||||
super(useragent);
|
||||
}
|
||||
|
||||
throw new Error('Unknown znca API provider');
|
||||
async genf(token: string, timestamp: string, uuid: string, type: FlapgIid) {
|
||||
const result = await genf(this.url + '/f', token, timestamp, uuid, type, this.useragent);
|
||||
|
||||
return {
|
||||
provider: 'nxapi' as const,
|
||||
url: this.url + '/f',
|
||||
token, timestamp, uuid, type,
|
||||
f: result.f,
|
||||
result,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export type FApi =
|
||||
'flapg' |
|
||||
'imink' |
|
||||
['nxapi', string];
|
||||
export async function f(
|
||||
token: string, timestamp: string | number, uuid: string, type: FlapgIid,
|
||||
useragent?: string
|
||||
): Promise<FResult> {
|
||||
const provider = getZncaApiFromEnvironment(useragent);
|
||||
|
||||
return provider.genf(token, '' + timestamp, uuid, type);
|
||||
}
|
||||
|
||||
export type FResult = {
|
||||
provider: string;
|
||||
@@ -264,21 +291,21 @@ export type FResult = {
|
||||
result: AndroidZncaFResponse;
|
||||
});
|
||||
|
||||
function getEnvApi(): FApi {
|
||||
function getZncaApiFromEnvironment(useragent?: string): ZncaApi {
|
||||
if (process.env.NXAPI_ZNCA_API) {
|
||||
if (process.env.NXAPI_ZNCA_API === 'flapg') {
|
||||
return 'flapg';
|
||||
return new ZncaApiFlapg(useragent);
|
||||
}
|
||||
if (process.env.NXAPI_ZNCA_API === 'imink') {
|
||||
return 'imink';
|
||||
return new ZncaApiImink(useragent);
|
||||
}
|
||||
|
||||
throw new Error('Unknown znca API provider');
|
||||
}
|
||||
|
||||
if (process.env.ZNCA_API_URL) {
|
||||
return ['nxapi', process.env.ZNCA_API_URL + '/f'];
|
||||
return new ZncaApiNxapi(process.env.ZNCA_API_URL, useragent);
|
||||
}
|
||||
|
||||
return 'flapg';
|
||||
return new ZncaApiFlapg(useragent);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import fetch from 'node-fetch';
|
||||
import createDebug from 'debug';
|
||||
import { WebServiceToken } from './znc-types.js';
|
||||
import { WebServiceToken } from './coral-types.js';
|
||||
import { NintendoAccountUser } from './na.js';
|
||||
import { ErrorResponse } from './util.js';
|
||||
import ZncApi from './znc.js';
|
||||
import CoralApi from './coral.js';
|
||||
import { WebServiceError, Users, AuthToken, UserProfile, Newspapers, Newspaper, Emoticons, Reaction, IslandProfile } from './nooklink-types.js';
|
||||
import { timeoutSignal } from '../util/misc.js';
|
||||
|
||||
@@ -76,8 +76,8 @@ export default class NooklinkApi {
|
||||
};
|
||||
}
|
||||
|
||||
static async createWithZnc(nso: ZncApi, user: NintendoAccountUser) {
|
||||
const data = await this.loginWithZnc(nso, user);
|
||||
static async createWithCoral(nso: CoralApi, user: NintendoAccountUser) {
|
||||
const data = await this.loginWithCoral(nso, user);
|
||||
|
||||
return {
|
||||
nooklink: new this(data.gtoken, data.useragent),
|
||||
@@ -85,7 +85,7 @@ export default class NooklinkApi {
|
||||
};
|
||||
}
|
||||
|
||||
static async loginWithZnc(nso: ZncApi, user: NintendoAccountUser) {
|
||||
static async loginWithCoral(nso: CoralApi, user: NintendoAccountUser) {
|
||||
const webserviceToken = await nso.getWebServiceToken(NOOKLINK_WEBSERVICE_ID);
|
||||
|
||||
return this.loginWithWebServiceToken(webserviceToken.result, user);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import fetch from 'node-fetch';
|
||||
import createDebug from 'debug';
|
||||
import { v4 as uuidgen } from 'uuid';
|
||||
import { WebServiceToken } from './znc-types.js';
|
||||
import { WebServiceToken } from './coral-types.js';
|
||||
import { NintendoAccountUser } from './na.js';
|
||||
import { ErrorResponse } from './util.js';
|
||||
import ZncApi from './znc.js';
|
||||
import CoralApi from './coral.js';
|
||||
import { ActiveFestivals, CoopResult, CoopResults, CoopSchedules, HeroRecords, NicknameAndIcons, PastFestivals, Records, Result, Results, Schedules, ShareResponse, ShopMerchandises, Stages, Timeline, WebServiceError, XPowerRankingSummary } from './splatnet2-types.js';
|
||||
import { timeoutSignal } from '../util/misc.js';
|
||||
import { toSeasonId, Rule as XPowerRankingRule } from './splatnet2-xrank.js';
|
||||
@@ -211,8 +211,8 @@ ${colour}
|
||||
});
|
||||
}
|
||||
|
||||
static async createWithZnc(nso: ZncApi, user: NintendoAccountUser) {
|
||||
const data = await this.loginWithZnc(nso, user);
|
||||
static async createWithCoral(nso: CoralApi, user: NintendoAccountUser) {
|
||||
const data = await this.loginWithCoral(nso, user);
|
||||
|
||||
return {
|
||||
splatnet: new this(data.iksm_session, data.useragent),
|
||||
@@ -220,7 +220,7 @@ ${colour}
|
||||
};
|
||||
}
|
||||
|
||||
static async loginWithZnc(nso: ZncApi, user: NintendoAccountUser) {
|
||||
static async loginWithCoral(nso: CoralApi, user: NintendoAccountUser) {
|
||||
const webserviceToken = await nso.getWebServiceToken(SPLATNET2_WEBSERVICE_ID);
|
||||
|
||||
return this.loginWithWebServiceToken(webserviceToken.result, user);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import fetch, { Response } from 'node-fetch';
|
||||
import createDebug from 'debug';
|
||||
import { ActiveEvent, Announcements, CurrentUser, Event, Friend, Presence, PresencePermissions, User, WebService, WebServiceToken, ZncErrorResponse, ZncStatus, ZncSuccessResponse } from './znc-types.js';
|
||||
import { ActiveEvent, Announcements, CurrentUser, Event, Friend, Presence, PresencePermissions, User, WebService, WebServiceToken, CoralErrorResponse, CoralStatus, CoralSuccessResponse } from './coral-types.js';
|
||||
import { ErrorResponse } from './util.js';
|
||||
import ZncApi from './znc.js';
|
||||
import CoralApi from './coral.js';
|
||||
import { version } from '../util/product.js';
|
||||
import { NintendoAccountUser } from './na.js';
|
||||
import { SavedToken } from '../common/auth/nso.js';
|
||||
@@ -10,11 +10,11 @@ import { timeoutSignal } from '../util/misc.js';
|
||||
|
||||
const debug = createDebug('nxapi:api:znc-proxy');
|
||||
|
||||
export default class ZncProxyApi implements ZncApi {
|
||||
export default class ZncProxyApi implements CoralApi {
|
||||
static useragent: string | null = null;
|
||||
|
||||
// Not used by ZncProxyApi
|
||||
onTokenExpired: ((data: ZncErrorResponse, res: Response) => Promise<void>) | null = null;
|
||||
onTokenExpired: ((data: CoralErrorResponse, res: Response) => Promise<void>) | null = null;
|
||||
/** @internal */
|
||||
_renewToken: Promise<void> | null = null;
|
||||
|
||||
@@ -51,64 +51,64 @@ export default class ZncProxyApi implements ZncApi {
|
||||
return data;
|
||||
}
|
||||
|
||||
async call<T = unknown>(url: string, parameter = {}): Promise<ZncSuccessResponse<T>> {
|
||||
async call<T = unknown>(url: string, parameter = {}): Promise<CoralSuccessResponse<T>> {
|
||||
throw new Error('Not supported in ZncProxyApi');
|
||||
}
|
||||
|
||||
async getAnnouncements() {
|
||||
const response = await this.fetch<{announcements: Announcements}>('/announcements');
|
||||
return {status: ZncStatus.OK as const, result: response.announcements, correlationId: ''};
|
||||
return {status: CoralStatus.OK as const, result: response.announcements, correlationId: ''};
|
||||
}
|
||||
|
||||
async getFriendList() {
|
||||
const response = await this.fetch<{friends: Friend[]}>('/friends');
|
||||
return {status: ZncStatus.OK as const, result: response, correlationId: ''};
|
||||
return {status: CoralStatus.OK as const, result: response, correlationId: ''};
|
||||
}
|
||||
|
||||
async addFavouriteFriend(nsaid: string) {
|
||||
await this.fetch('/friend/' + nsaid, 'POST', JSON.stringify({
|
||||
isFavoriteFriend: true,
|
||||
}));
|
||||
return {status: ZncStatus.OK as const, result: {}, correlationId: ''};
|
||||
return {status: CoralStatus.OK as const, result: {}, correlationId: ''};
|
||||
}
|
||||
|
||||
async removeFavouriteFriend(nsaid: string) {
|
||||
await this.fetch('/friend/' + nsaid, 'POST', JSON.stringify({
|
||||
isFavoriteFriend: false,
|
||||
}));
|
||||
return {status: ZncStatus.OK as const, result: {}, correlationId: ''};
|
||||
return {status: CoralStatus.OK as const, result: {}, correlationId: ''};
|
||||
}
|
||||
|
||||
async getWebServices() {
|
||||
const response = await this.fetch<{webservices: WebService[]}>('/webservices');
|
||||
return {status: ZncStatus.OK as const, result: response.webservices, correlationId: ''};
|
||||
return {status: CoralStatus.OK as const, result: response.webservices, correlationId: ''};
|
||||
}
|
||||
|
||||
async getActiveEvent() {
|
||||
const response = await this.fetch<{activeevent: ActiveEvent}>('/activeevent');
|
||||
return {status: ZncStatus.OK as const, result: response.activeevent, correlationId: ''};
|
||||
return {status: CoralStatus.OK as const, result: response.activeevent, correlationId: ''};
|
||||
}
|
||||
|
||||
async getEvent(id: number) {
|
||||
const response = await this.fetch<{event: Event}>('/event/' + id);
|
||||
return {status: ZncStatus.OK as const, result: response.event, correlationId: ''};
|
||||
return {status: CoralStatus.OK as const, result: response.event, correlationId: ''};
|
||||
}
|
||||
|
||||
async getUser(id: number) {
|
||||
const response = await this.fetch<{user: User}>('/user/' + id);
|
||||
return {status: ZncStatus.OK as const, result: response.user, correlationId: ''};
|
||||
return {status: CoralStatus.OK as const, result: response.user, correlationId: ''};
|
||||
}
|
||||
|
||||
async getCurrentUser() {
|
||||
const response = await this.fetch<{user: CurrentUser}>('/user');
|
||||
return {status: ZncStatus.OK as const, result: response.user, correlationId: ''};
|
||||
return {status: CoralStatus.OK as const, result: response.user, correlationId: ''};
|
||||
}
|
||||
|
||||
async getCurrentUserPermissions() {
|
||||
const user = await this.getCurrentUser();
|
||||
|
||||
return {
|
||||
status: ZncStatus.OK as const,
|
||||
status: CoralStatus.OK as const,
|
||||
result: {
|
||||
etag: user.result.etag,
|
||||
permissions: user.result.permissions,
|
||||
@@ -119,16 +119,16 @@ export default class ZncProxyApi implements ZncApi {
|
||||
|
||||
async updateCurrentUserPermissions(
|
||||
to: PresencePermissions, from: PresencePermissions, etag: string
|
||||
): Promise<ZncSuccessResponse<{}>> {
|
||||
): Promise<CoralSuccessResponse<{}>> {
|
||||
throw new Error('Not supported in ZncProxyApi');
|
||||
}
|
||||
|
||||
async getWebServiceToken(id: string) {
|
||||
const response = await this.fetch<{token: WebServiceToken}>('/webservice/' + id + '/token');
|
||||
return {status: ZncStatus.OK as const, result: response.token, correlationId: ''};
|
||||
return {status: CoralStatus.OK as const, result: response.token, correlationId: ''};
|
||||
}
|
||||
|
||||
async getToken(token: string, user: NintendoAccountUser): ReturnType<ZncApi['getToken']> {
|
||||
async getToken(token: string, user: NintendoAccountUser): ReturnType<CoralApi['getToken']> {
|
||||
throw new Error('Not supported in ZncProxyApi');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user