mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-22 20:48:56 +01:00
Merge branch 'master' of https://github.com/tgstation/-tg-station into goonchatv2
This commit is contained in:
@@ -63,7 +63,7 @@
|
||||
|
||||
if(!SSdbcore.Connect())
|
||||
log_world("Ban database connection failure. Key [ckeytext] not checked")
|
||||
GLOB.diary << "Ban database connection failure. Key [ckeytext] not checked"
|
||||
GLOB.world_game_log << "Ban database connection failure. Key [ckeytext] not checked"
|
||||
return
|
||||
|
||||
var/ipquery = ""
|
||||
|
||||
@@ -610,20 +610,41 @@
|
||||
set desc = "(atom path) Spawn an atom"
|
||||
set name = "Spawn"
|
||||
|
||||
return usr.client.spawn_atom_impl(object, FALSE)
|
||||
|
||||
/datum/admins/proc/spawn_atom_adv(object as text)
|
||||
set category = "Debug"
|
||||
set desc = "(atom path) Spawn an atom with New() parameters"
|
||||
set name = "Advanced Spawn"
|
||||
|
||||
return usr.client.spawn_atom_impl(object, TRUE)
|
||||
|
||||
|
||||
/client/proc/spawn_atom_impl(object, params)
|
||||
if(!check_rights(R_SPAWN))
|
||||
return
|
||||
|
||||
var/chosen = pick_closest_path(object)
|
||||
if(!chosen)
|
||||
return
|
||||
|
||||
var/list/arguments
|
||||
if(ispath(chosen,/turf))
|
||||
var/turf/T = get_turf(usr.loc)
|
||||
T.ChangeTurf(chosen)
|
||||
else
|
||||
var/atom/A = new chosen(usr.loc)
|
||||
if(params)
|
||||
arguments = usr.client.get_callproc_args(TRUE)
|
||||
|
||||
if(!usr)
|
||||
return
|
||||
|
||||
arguments = list(usr.loc) + arguments
|
||||
|
||||
var/atom/A = new chosen(arglist(arguments))
|
||||
A.admin_spawned = TRUE
|
||||
|
||||
log_admin("[key_name(usr)] spawned [chosen] at ([usr.x],[usr.y],[usr.z])")
|
||||
log_admin("[key_name(usr)] spawned [chosen] at [COORD(usr)][LAZYLEN(arguments) > 1 ? " with parameters [print_single_line(arguments)]": ""]")
|
||||
SSblackbox.add_details("admin_verb","Spawn Atom") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
|
||||
|
||||
@@ -1,50 +1,21 @@
|
||||
//By Carnwennan
|
||||
|
||||
//This system was made as an alternative to all the in-game lists and variables used to log stuff in-game.
|
||||
//lists and variables are great. However, they have several major flaws:
|
||||
//Firstly, they use memory. TGstation has one of the highest memory usage of all the ss13 branches.
|
||||
//Secondly, they are usually stored in an object. This means that they aren't centralised. It also means that
|
||||
//the data is lost when the object is deleted! This is especially annoying for things like the singulo engine!
|
||||
#define INVESTIGATE_DIR "data/investigate/"
|
||||
|
||||
//SYSTEM
|
||||
/proc/investigate_subject2file(subject)
|
||||
return file("[INVESTIGATE_DIR][subject].html")
|
||||
|
||||
/proc/investigate_reset()
|
||||
if(fdel(INVESTIGATE_DIR))
|
||||
return 1
|
||||
return 0
|
||||
|
||||
/atom/proc/investigate_log(message, subject)
|
||||
if(!message)
|
||||
return
|
||||
var/F = investigate_subject2file(subject)
|
||||
if(!F)
|
||||
atom/proc/investigate_log(message, subject)
|
||||
if(!message || !subject)
|
||||
return
|
||||
var/F = file("[GLOB.log_directory]/[subject].html")
|
||||
F << "<small>[time_stamp()] \ref[src] ([x],[y],[z])</small> || [src] [message]<br>"
|
||||
|
||||
//ADMINVERBS
|
||||
|
||||
/client/proc/investigate_show( subject in list("hrefs","notes, memos, watchlist","singulo","wires","telesci", "gravity", "records", "cargo", "supermatter", "atmos", "experimentor", "botany") )
|
||||
set name = "Investigate"
|
||||
set category = "Admin"
|
||||
if(!holder)
|
||||
return
|
||||
switch(subject)
|
||||
if("singulo", "wires", "telesci", "gravity", "records", "cargo", "supermatter", "atmos", "botany") //general one-round-only stuff
|
||||
var/F = investigate_subject2file(subject)
|
||||
if(!F)
|
||||
to_chat(src, "<font color='red'>Error: admin_investigate: [INVESTIGATE_DIR][subject] is an invalid path or cannot be accessed.</font>")
|
||||
return
|
||||
src << browse(F,"window=investigate[subject];size=800x300")
|
||||
if("hrefs") //persistent logs and stuff
|
||||
if(GLOB.href_logfile)
|
||||
src << browse(GLOB.href_logfile,"window=investigate[subject];size=800x300")
|
||||
else if(!config.log_hrefs)
|
||||
to_chat(src, "<span class='danger'>Href logging is off and no logfile was found.</span>")
|
||||
return
|
||||
else
|
||||
to_chat(src, "<span class='danger'>No href logfile was found.</span>")
|
||||
return
|
||||
if("notes, memos, watchlist")
|
||||
browse_messages()
|
||||
else
|
||||
var/F = file("[GLOB.log_directory]/[subject].html")
|
||||
if(!fexists(F))
|
||||
to_chat(src, "<span class='danger'>No [subject] logfile was found.</span>")
|
||||
return
|
||||
src << browse(F,"window=investigate[subject];size=800x300")
|
||||
@@ -127,7 +127,7 @@ GLOBAL_PROTECT(admin_ranks)
|
||||
else
|
||||
if(!SSdbcore.Connect())
|
||||
log_world("Failed to connect to database in load_admin_ranks(). Reverting to legacy system.")
|
||||
GLOB.diary << "Failed to connect to database in load_admin_ranks(). Reverting to legacy system."
|
||||
GLOB.world_game_log << "Failed to connect to database in load_admin_ranks(). Reverting to legacy system."
|
||||
config.admin_legacy_system = 1
|
||||
load_admin_ranks()
|
||||
return
|
||||
@@ -202,7 +202,7 @@ GLOBAL_PROTECT(admin_ranks)
|
||||
else
|
||||
if(!SSdbcore.Connect())
|
||||
log_world("Failed to connect to database in load_admins(). Reverting to legacy system.")
|
||||
GLOB.diary << "Failed to connect to database in load_admins(). Reverting to legacy system."
|
||||
GLOB.world_game_log << "Failed to connect to database in load_admins(). Reverting to legacy system."
|
||||
config.admin_legacy_system = 1
|
||||
load_admins()
|
||||
return
|
||||
|
||||
@@ -43,16 +43,14 @@ GLOBAL_LIST_INIT(admin_verbs_admin, world.AVerbsAdmin())
|
||||
/datum/admins/proc/set_admin_notice,/*announcement all clients see when joining the server.*/
|
||||
/client/proc/admin_ghost, /*allows us to ghost/reenter body at will*/
|
||||
/client/proc/toggle_view_range, /*changes how far we can see*/
|
||||
/datum/admins/proc/view_txt_log, /*shows the server log (diary) for today*/
|
||||
/datum/admins/proc/view_txt_log, /*shows the server log (world_game_log) for today*/
|
||||
/datum/admins/proc/view_atk_log, /*shows the server combat-log, doesn't do anything presently*/
|
||||
/client/proc/cmd_admin_subtle_message, /*send an message to somebody as a 'voice in their head'*/
|
||||
/client/proc/cmd_admin_delete, /*delete an instance/object/mob/etc*/
|
||||
/client/proc/cmd_admin_check_contents, /*displays the contents of an instance*/
|
||||
/client/proc/check_antagonists, /*shows all antags*/
|
||||
/datum/admins/proc/access_news_network, /*allows access of newscasters*/
|
||||
/client/proc/giveruntimelog, /*allows us to give access to runtime logs to somebody*/
|
||||
/client/proc/getruntimelog, /*allows us to access runtime logs to somebody*/
|
||||
/client/proc/getserverlog, /*allows us to fetch server logs (diary) for other days*/
|
||||
/client/proc/getserverlog, /*allows us to fetch server logs (world_game_log) for other days*/
|
||||
/client/proc/jumptocoord, /*we ghost and jump to a coordinate*/
|
||||
/client/proc/Getmob, /*teleports a mob to our location*/
|
||||
/client/proc/Getkey, /*teleports a mob with a certain ckey to our location*/
|
||||
@@ -103,7 +101,7 @@ GLOBAL_LIST_INIT(admin_verbs_fun, list(
|
||||
/client/proc/smite
|
||||
))
|
||||
GLOBAL_PROTECT(admin_verbs_spawn)
|
||||
GLOBAL_LIST_INIT(admin_verbs_spawn, list(/datum/admins/proc/spawn_atom,/client/proc/respawn_character))
|
||||
GLOBAL_LIST_INIT(admin_verbs_spawn, list(/datum/admins/proc/spawn_atom,/datum/admins/proc/spawn_atom_adv,/client/proc/respawn_character))
|
||||
GLOBAL_PROTECT(admin_verbs_server)
|
||||
GLOBAL_LIST_INIT(admin_verbs_server, world.AVerbsServer())
|
||||
/world/proc/AVerbsServer()
|
||||
@@ -113,7 +111,6 @@ GLOBAL_LIST_INIT(admin_verbs_server, world.AVerbsServer())
|
||||
/datum/admins/proc/end_round,
|
||||
/datum/admins/proc/delay,
|
||||
/datum/admins/proc/toggleaban,
|
||||
/client/proc/toggle_log_hrefs,
|
||||
/client/proc/everyone_random,
|
||||
/datum/admins/proc/toggleAI,
|
||||
/client/proc/cmd_admin_delete, /*delete an instance/object/mob/etc*/
|
||||
@@ -216,7 +213,6 @@ GLOBAL_LIST_INIT(admin_verbs_hideable, list(
|
||||
/datum/admins/proc/restart,
|
||||
/datum/admins/proc/delay,
|
||||
/datum/admins/proc/toggleaban,
|
||||
/client/proc/toggle_log_hrefs,
|
||||
/client/proc/everyone_random,
|
||||
/datum/admins/proc/toggleAI,
|
||||
/client/proc/restart_controller,
|
||||
@@ -631,19 +627,6 @@ GLOBAL_LIST_INIT(admin_verbs_hideable, list(
|
||||
togglebuildmode(src.mob)
|
||||
SSblackbox.add_details("admin_verb","Toggle Build Mode") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
/client/proc/toggle_log_hrefs()
|
||||
set name = "Toggle href logging"
|
||||
set category = "Server"
|
||||
if(!holder)
|
||||
return
|
||||
if(config)
|
||||
if(config.log_hrefs)
|
||||
config.log_hrefs = 0
|
||||
to_chat(src, "<b>Stopped logging hrefs</b>")
|
||||
else
|
||||
config.log_hrefs = 1
|
||||
to_chat(src, "<b>Started logging hrefs</b>")
|
||||
|
||||
/client/proc/check_ai_laws()
|
||||
set name = "Check AI Laws"
|
||||
set category = "Admin"
|
||||
|
||||
@@ -67,7 +67,7 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new)
|
||||
for(var/I in l2b)
|
||||
var/datum/admin_help/AH = I
|
||||
dat += "<span class='adminnotice'><span class='adminhelp'>Ticket #[AH.id]</span>: <A HREF='?_src_=holder;ahelp=\ref[AH];ahelp_action=ticket'>[AH.initiator_key_name]: [AH.name]</A></span><br>"
|
||||
|
||||
|
||||
usr << browse(dat.Join(), "window=ahelp_list[state];size=600x480")
|
||||
|
||||
//Tickets statpanel
|
||||
@@ -253,7 +253,7 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new)
|
||||
if(state == AHELP_ACTIVE)
|
||||
to_chat(usr, "<span class='warning'>This ticket is already open.</span>")
|
||||
return
|
||||
|
||||
|
||||
if(GLOB.ahelp_tickets.CKey2ActiveTicket(initiator_ckey))
|
||||
to_chat(usr, "<span class='warning'>This user already has an active ticket, cannot reopen this one.</span>")
|
||||
return
|
||||
@@ -310,7 +310,7 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new)
|
||||
RemoveActive()
|
||||
state = AHELP_RESOLVED
|
||||
GLOB.ahelp_tickets.ListInsert(src)
|
||||
|
||||
|
||||
if(initiator)
|
||||
initiator.giveadminhelpverb()
|
||||
|
||||
@@ -325,7 +325,7 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new)
|
||||
/datum/admin_help/proc/Reject(key_name = key_name_admin(usr))
|
||||
if(state != AHELP_ACTIVE)
|
||||
return
|
||||
|
||||
|
||||
if(initiator)
|
||||
initiator.giveadminhelpverb()
|
||||
|
||||
@@ -479,7 +479,7 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new)
|
||||
current_ticket.MessageNoRecipient(msg)
|
||||
current_ticket.TimeoutVerb()
|
||||
return
|
||||
else
|
||||
else
|
||||
to_chat(usr, "<span class='warning'>Ticket not found, creating new one...</span>")
|
||||
else
|
||||
current_ticket.AddInteraction("[key_name_admin(usr)] opened a new ticket.")
|
||||
@@ -494,7 +494,7 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new)
|
||||
|
||||
if(!check_rights(R_ADMIN, TRUE))
|
||||
return
|
||||
|
||||
|
||||
var/browse_to
|
||||
|
||||
switch(input("Display which ticket list?") as null|anything in list("Active Tickets", "Closed Tickets", "Resolved Tickets"))
|
||||
@@ -506,7 +506,7 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new)
|
||||
browse_to = AHELP_RESOLVED
|
||||
else
|
||||
return
|
||||
|
||||
|
||||
GLOB.ahelp_tickets.BrowseTickets(browse_to)
|
||||
|
||||
//
|
||||
|
||||
@@ -145,8 +145,8 @@ GLOBAL_PROTECT(AdminProcCallCount)
|
||||
|
||||
|
||||
|
||||
/client/proc/get_callproc_args()
|
||||
var/argnum = input("Number of arguments","Number:",0) as num|null
|
||||
/client/proc/get_callproc_args(is_atom_new = FALSE)
|
||||
var/argnum = input("Number of arguments[is_atom_new ? " (Excluding loc)" : ""]","Number:",0) as num|null
|
||||
if(isnull(argnum))
|
||||
return
|
||||
|
||||
|
||||
@@ -1,62 +1,3 @@
|
||||
/*
|
||||
HOW DO I LOG RUNTIMES?
|
||||
Firstly, start dreamdeamon if it isn't already running. Then select "world>Log Session" (or press the F3 key)
|
||||
navigate the popup window to the data/logs/runtimes/ folder from where your tgstation .dmb is located.
|
||||
(you may have to make this folder yourself)
|
||||
|
||||
OPTIONAL: you can select the little checkbox down the bottom to make dreamdeamon save the log everytime you
|
||||
start a world. Just remember to repeat these steps with a new name when you update to a new revision!
|
||||
|
||||
Save it with the name of the revision your server uses (e.g. r3459.txt).
|
||||
Game Masters will now be able to grant access any runtime logs you have archived this way!
|
||||
This will allow us to gather information on bugs across multiple servers and make maintaining the TG
|
||||
codebase for the entire /TG/station commuity a TONNE easier :3 Thanks for your help!
|
||||
*/
|
||||
|
||||
|
||||
//This proc allows Game Masters to grant a client access to the .getruntimelog verb
|
||||
//Permissions expire at the end of each round.
|
||||
//Runtimes can be used to meta or spot game-crashing exploits so it's advised to only grant coders that
|
||||
//you trust access. Also, it may be wise to ensure that they are not going to play in the current round.
|
||||
/client/proc/giveruntimelog()
|
||||
set name = ".giveruntimelog"
|
||||
set desc = "Give somebody access to any session logfiles saved to the /log/runtime/ folder."
|
||||
set category = null
|
||||
|
||||
if(!src.holder)
|
||||
to_chat(src, "<font color='red'>Only Admins may use this command.</font>")
|
||||
return
|
||||
|
||||
var/client/target = input(src,"Choose somebody to grant access to the server's runtime logs (permissions expire at the end of each round):","Grant Permissions",null) as null|anything in GLOB.clients
|
||||
if(!istype(target,/client))
|
||||
to_chat(src, "<font color='red'>Error: giveruntimelog(): Client not found.</font>")
|
||||
return
|
||||
|
||||
target.verbs |= /client/proc/getruntimelog
|
||||
to_chat(target, "<font color='red'>You have been granted access to runtime logs. Please use them responsibly or risk being banned.</font>")
|
||||
return
|
||||
|
||||
|
||||
//This proc allows download of runtime logs saved within the data/logs/ folder by dreamdeamon.
|
||||
//It works similarly to show-server-log.
|
||||
/client/proc/getruntimelog()
|
||||
set name = ".getruntimelog"
|
||||
set desc = "Retrieve any session logfiles saved by dreamdeamon."
|
||||
set category = null
|
||||
|
||||
var/path = browse_files("data/logs/runtimes/")
|
||||
if(!path)
|
||||
return
|
||||
|
||||
if(file_spam_check())
|
||||
return
|
||||
|
||||
message_admins("[key_name_admin(src)] accessed file: [path]")
|
||||
src << ftp(file(path))
|
||||
to_chat(src, "Attempting to send file, this may take a fair few minutes if the file is very large.")
|
||||
return
|
||||
|
||||
|
||||
//This proc allows download of past server logs saved within the data/logs/ folder.
|
||||
//It works similarly to show-server-log.
|
||||
/client/proc/getserverlog()
|
||||
@@ -83,10 +24,10 @@
|
||||
/datum/admins/proc/view_txt_log()
|
||||
set category = "Admin"
|
||||
set name = "Show Server Log"
|
||||
set desc = "Shows today's server log."
|
||||
set desc = "Shows server log for this round."
|
||||
|
||||
if(fexists("[GLOB.diary]"))
|
||||
src << ftp(GLOB.diary)
|
||||
if(fexists("[GLOB.world_game_log]"))
|
||||
src << ftp(GLOB.world_game_log)
|
||||
else
|
||||
to_chat(src, "<font color='red'>Server log not found, try using .getserverlog.</font>")
|
||||
return
|
||||
@@ -97,10 +38,10 @@
|
||||
/datum/admins/proc/view_atk_log()
|
||||
set category = "Admin"
|
||||
set name = "Show Server Attack Log"
|
||||
set desc = "Shows today's server attack log."
|
||||
set desc = "Shows server attack log for this round."
|
||||
|
||||
if(fexists("[GLOB.diaryofmeanpeople]"))
|
||||
src << ftp(GLOB.diaryofmeanpeople)
|
||||
if(fexists("[GLOB.world_attack_log]"))
|
||||
src << ftp(GLOB.world_attack_log)
|
||||
else
|
||||
to_chat(src, "<font color='red'>Server attack log not found, try using .getserverlog.</font>")
|
||||
return
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
usr.client.images += preview
|
||||
if(alert(usr,"Confirm location.","Template Confirm","Yes","No") == "Yes")
|
||||
if(template.load(T, centered = TRUE))
|
||||
message_admins("<span class='adminnotice'>[key_name_admin(usr)] has placed a map template ([template.name]) at <A HREF='?_src_=holder;adminplayerobservecoodjump=1;X=[T.x];Y=[T.y];Z=[T.z]'>(JMP)</a></span>")
|
||||
message_admins("<span class='adminnotice'>[key_name_admin(usr)] has placed a map template ([template.name]) at [ADMIN_COORDJMP(T)]</span>")
|
||||
else
|
||||
to_chat(usr, "Failed to place map")
|
||||
usr.client.images -= preview
|
||||
|
||||
@@ -213,7 +213,14 @@ GLOBAL_PROTECT(VVpixelmovement)
|
||||
.["class"] = null
|
||||
return
|
||||
.["type"] = type
|
||||
.["value"] = new type()
|
||||
|
||||
var/list/arguments
|
||||
if(alert(usr, "Would you like to add arguments?", "New Atom", "No", "Yes") == "Yes")
|
||||
arguments = get_callproc_args(FALSE)
|
||||
else
|
||||
arguments = list()
|
||||
|
||||
.["value"] = new type(arglist(arguments))
|
||||
|
||||
if (VV_NEW_DATUM)
|
||||
var/type = pick_closest_path(FALSE, get_fancy_list_of_datum_types())
|
||||
@@ -221,7 +228,13 @@ GLOBAL_PROTECT(VVpixelmovement)
|
||||
.["class"] = null
|
||||
return
|
||||
.["type"] = type
|
||||
.["value"] = new type()
|
||||
var/list/arguments
|
||||
if(alert(usr, "Would you like to add arguments?", "New Atom", "No", "Yes") == "Yes")
|
||||
arguments = get_callproc_args(FALSE)
|
||||
else
|
||||
arguments = list()
|
||||
|
||||
.["value"] = new type(arglist(arguments))
|
||||
|
||||
if (VV_NEW_TYPE)
|
||||
var/type = current_value
|
||||
@@ -237,7 +250,14 @@ GLOBAL_PROTECT(VVpixelmovement)
|
||||
.["class"] = null
|
||||
return
|
||||
.["type"] = type
|
||||
.["value"] = new type()
|
||||
|
||||
var/list/arguments
|
||||
if(alert(usr, "Would you like to add arguments?", "New Atom", "No", "Yes") == "Yes")
|
||||
arguments = get_callproc_args(FALSE);
|
||||
else
|
||||
arguments = list()
|
||||
|
||||
.["value"] = new type(arglist(arguments))
|
||||
|
||||
|
||||
if (VV_NEW_LIST)
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
var/res = alert(usr, "Show the title of this song to the players?",, "No", "Yes", "Cancel")
|
||||
switch(res)
|
||||
if("Yes")
|
||||
to_chat(world, "An admin played: [S]")
|
||||
to_chat(world, "<span class='boldannounce'>An admin played: [S]</span>")
|
||||
if("Cancel")
|
||||
return
|
||||
|
||||
|
||||
@@ -108,8 +108,8 @@
|
||||
for(var/mob/M in view(range,A))
|
||||
to_chat(M, msg)
|
||||
|
||||
log_admin("LocalNarrate: [key_name(usr)] at ([get_area(A)]): [msg]")
|
||||
message_admins("<span class='adminnotice'><b> LocalNarrate: [key_name_admin(usr)] at (<A HREF='?_src_=holder;adminplayerobservecoodjump=1;X=[A.x];Y=[A.y];Z=[A.z]'>[get_area(A)]</a>):</b> [msg]<BR></span>")
|
||||
log_admin("LocalNarrate: [key_name(usr)] at [get_area(A)][COORD(A)]: [msg]")
|
||||
message_admins("<span class='adminnotice'><b> LocalNarrate: [key_name_admin(usr)] at [get_area(A)][ADMIN_JMP(A)]:</b> [msg]<BR></span>")
|
||||
SSblackbox.add_details("admin_verb","Local Narrate") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
|
||||
|
||||
/client/proc/cmd_admin_godmode(mob/M in GLOB.mob_list)
|
||||
@@ -350,7 +350,6 @@ Traitors and the like can also be revived with the previous role mostly intact.
|
||||
|
||||
if(G_found.mind && !G_found.mind.active)
|
||||
G_found.mind.transfer_to(new_character) //be careful when doing stuff like this! I've already checked the mind isn't in use
|
||||
new_character.mind.special_verbs = list()
|
||||
else
|
||||
new_character.mind_initialize()
|
||||
if(!new_character.mind.assigned_role)
|
||||
|
||||
Reference in New Issue
Block a user