Merge branch 'develop' of ssh.gitgud.io:lambadalambda/pleroma into feature/unfollow-by-screen-name
This commit is contained in:
commit
f6547f7b7f
@ -18,6 +18,31 @@ defmodule Pleroma.Upload do
|
||||
}
|
||||
end
|
||||
|
||||
def store(%{"img" => "data:image/" <> image_data}) do
|
||||
parsed = Regex.named_captures(~r/(?<filetype>jpeg|png|gif);base64,(?<data>.*)/, image_data)
|
||||
data = Base.decode64!(parsed["data"])
|
||||
uuid = Ecto.UUID.generate
|
||||
upload_folder = Path.join(upload_path(), uuid)
|
||||
File.mkdir_p!(upload_folder)
|
||||
filename = Base.encode16(:crypto.hash(:sha256, data)) <> ".#{parsed["filetype"]}"
|
||||
result_file = Path.join(upload_folder, filename)
|
||||
|
||||
File.write!(result_file, data)
|
||||
|
||||
content_type = "image/#{parsed["filetype"]}"
|
||||
|
||||
%{
|
||||
"type" => "Image",
|
||||
"url" => [%{
|
||||
"type" => "Link",
|
||||
"mediaType" => content_type,
|
||||
"href" => url_for(Path.join(uuid, filename))
|
||||
}],
|
||||
"name" => filename,
|
||||
"uuid" => uuid
|
||||
}
|
||||
end
|
||||
|
||||
defp upload_path do
|
||||
Application.get_env(:pleroma, Pleroma.Upload)
|
||||
|> Keyword.fetch!(:uploads)
|
||||
|
@ -13,6 +13,7 @@ defmodule Pleroma.User do
|
||||
field :password_confirmation, :string, virtual: true
|
||||
field :following, { :array, :string }, default: []
|
||||
field :ap_id, :string
|
||||
field :avatar, :map
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
@ -167,7 +167,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
||||
Repo.all(query)
|
||||
end
|
||||
|
||||
def upload(%Plug.Upload{} = file) do
|
||||
def upload(file) do
|
||||
data = Upload.store(file)
|
||||
Repo.insert(%Object{data: data})
|
||||
end
|
||||
|
@ -45,5 +45,6 @@ defmodule Pleroma.Web.Router do
|
||||
post "/favorites/create", TwitterAPI.Controller, :favorite
|
||||
post "/favorites/destroy/:id", TwitterAPI.Controller, :unfavorite
|
||||
post "/statuses/retweet/:id", TwitterAPI.Controller, :retweet
|
||||
post "/qvitter/update_avatar", TwitterAPI.Controller, :update_avatar
|
||||
end
|
||||
end
|
||||
|
@ -4,8 +4,10 @@ defmodule Pleroma.Web.TwitterAPI.Representers.UserRepresenter do
|
||||
alias Pleroma.User
|
||||
|
||||
def to_map(user, opts) do
|
||||
|
||||
image = "https://placehold.it/48x48"
|
||||
image = case user.avatar do
|
||||
%{"url" => [%{"href" => href} | _]} -> href
|
||||
_ -> "https://placehold.it/48x48"
|
||||
end
|
||||
|
||||
following = if opts[:for] do
|
||||
User.following?(opts[:for], user)
|
||||
|
@ -3,6 +3,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
|
||||
alias Pleroma.Web.TwitterAPI.TwitterAPI
|
||||
alias Pleroma.Web.TwitterAPI.Representers.{UserRepresenter, ActivityRepresenter}
|
||||
alias Pleroma.{Repo, Activity}
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
|
||||
def verify_credentials(%{assigns: %{user: user}} = conn, _params) do
|
||||
response = user |> UserRepresenter.to_json(%{for: user})
|
||||
@ -142,6 +143,18 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
|
||||
end
|
||||
end
|
||||
|
||||
def update_avatar(%{assigns: %{user: user}} = conn, params) do
|
||||
{:ok, object} = ActivityPub.upload(params)
|
||||
change = Ecto.Changeset.change(user, %{avatar: object.data})
|
||||
{:ok, user} = Repo.update(change)
|
||||
|
||||
response = UserRepresenter.to_map(user, %{for: user})
|
||||
|> Poison.encode!
|
||||
|
||||
conn
|
||||
|> json_reply(200, response)
|
||||
end
|
||||
|
||||
defp json_reply(conn, status, json) do
|
||||
conn
|
||||
|> put_resp_content_type("application/json")
|
||||
|
@ -0,0 +1,9 @@
|
||||
defmodule Pleroma.Repo.Migrations.AddAvatarObjectToUsers do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:users) do
|
||||
add :avatar, :map
|
||||
end
|
||||
end
|
||||
end
|
@ -1 +1 @@
|
||||
<!DOCTYPE html><html><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Pleroma</title><link rel=stylesheet href=/static/font/css/fontello.css><link rel=stylesheet href=/static/font/css/animation.css><link href=/static/css/app.a86c13eb46180b1b975a1acd59b52cc6.css rel=stylesheet></head><body style="display: none"><div id=app></div><script type=text/javascript src=/static/js/manifest.da7ea91e505330123f38.js></script><script type=text/javascript src=/static/js/vendor.d7d8813599feb765b152.js></script><script type=text/javascript src=/static/js/app.dcc60205ebdef9eb3d87.js></script></body></html>
|
||||
<!DOCTYPE html><html><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Pleroma</title><link rel=stylesheet href=/static/font/css/fontello.css><link rel=stylesheet href=/static/font/css/animation.css><link href=/static/css/app.a86c13eb46180b1b975a1acd59b52cc6.css rel=stylesheet></head><body style="display: none"><div id=app></div><script type=text/javascript src=/static/js/manifest.66d994092e61600982a8.js></script><script type=text/javascript src=/static/js/vendor.d7d8813599feb765b152.js></script><script type=text/javascript src=/static/js/app.d4e0a640b375c4b52997.js></script></body></html>
|
4
priv/static/static/js/app.d4e0a640b375c4b52997.js
Normal file
4
priv/static/static/js/app.d4e0a640b375c4b52997.js
Normal file
File diff suppressed because one or more lines are too long
1
priv/static/static/js/app.d4e0a640b375c4b52997.js.map
Normal file
1
priv/static/static/js/app.d4e0a640b375c4b52997.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
priv/static/static/js/manifest.66d994092e61600982a8.js
Normal file
2
priv/static/static/js/manifest.66d994092e61600982a8.js
Normal file
@ -0,0 +1,2 @@
|
||||
!function(e){function t(n){if(r[n])return r[n].exports;var a=r[n]={exports:{},id:n,loaded:!1};return e[n].call(a.exports,a,a.exports,t),a.loaded=!0,a.exports}var n=window.webpackJsonp;window.webpackJsonp=function(o,c){for(var p,s,l=0,i=[];l<o.length;l++)s=o[l],a[s]&&i.push.apply(i,a[s]),a[s]=0;for(p in c)e[p]=c[p];for(n&&n(o,c);i.length;)i.shift().call(null,t);if(c[0])return r[0]=0,t(0)};var r={},a={0:0};t.e=function(e,n){if(0===a[e])return n.call(null,t);if(void 0!==a[e])a[e].push(n);else{a[e]=[n];var r=document.getElementsByTagName("head")[0],o=document.createElement("script");o.type="text/javascript",o.charset="utf-8",o.async=!0,o.src=t.p+"static/js/"+e+"."+{1:"d7d8813599feb765b152",2:"d4e0a640b375c4b52997"}[e]+".js",r.appendChild(o)}},t.m=e,t.c=r,t.p="/"}([]);
|
||||
//# sourceMappingURL=manifest.66d994092e61600982a8.js.map
|
File diff suppressed because one or more lines are too long
@ -1,2 +0,0 @@
|
||||
!function(e){function t(n){if(r[n])return r[n].exports;var a=r[n]={exports:{},id:n,loaded:!1};return e[n].call(a.exports,a,a.exports,t),a.loaded=!0,a.exports}var n=window.webpackJsonp;window.webpackJsonp=function(c,o){for(var p,s,l=0,d=[];l<c.length;l++)s=c[l],a[s]&&d.push.apply(d,a[s]),a[s]=0;for(p in o)e[p]=o[p];for(n&&n(c,o);d.length;)d.shift().call(null,t);if(o[0])return r[0]=0,t(0)};var r={},a={0:0};t.e=function(e,n){if(0===a[e])return n.call(null,t);if(void 0!==a[e])a[e].push(n);else{a[e]=[n];var r=document.getElementsByTagName("head")[0],c=document.createElement("script");c.type="text/javascript",c.charset="utf-8",c.async=!0,c.src=t.p+"static/js/"+e+"."+{1:"d7d8813599feb765b152",2:"dcc60205ebdef9eb3d87"}[e]+".js",r.appendChild(c)}},t.m=e,t.c=r,t.p="/"}([]);
|
||||
//# sourceMappingURL=manifest.da7ea91e505330123f38.js.map
|
@ -3,7 +3,7 @@ defmodule Pleroma.Builders.ActivityBuilder do
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
|
||||
def build(data \\ %{}, opts \\ %{}) do
|
||||
user = opts[:user] || UserBuilder.build
|
||||
user = opts[:user] || Pleroma.Factory.insert(:user)
|
||||
activity = %{
|
||||
"id" => 1,
|
||||
"actor" => user.ap_id,
|
||||
@ -29,7 +29,7 @@ defmodule Pleroma.Builders.ActivityBuilder do
|
||||
end
|
||||
|
||||
def public_and_non_public do
|
||||
{:ok, user} = UserBuilder.insert
|
||||
user = Pleroma.Factory.insert(:user)
|
||||
|
||||
public = build(%{"id" => 1}, %{user: user})
|
||||
non_public = build(%{"id" => 2, "to" => []}, %{user: user})
|
||||
|
@ -3,6 +3,8 @@ defmodule Pleroma.UserTest do
|
||||
alias Pleroma.User
|
||||
use Pleroma.DataCase
|
||||
|
||||
import Pleroma.Factory
|
||||
|
||||
test "ap_id returns the activity pub id for the user" do
|
||||
host =
|
||||
Application.get_env(:pleroma, Pleroma.Web.Endpoint)
|
||||
@ -25,21 +27,21 @@ defmodule Pleroma.UserTest do
|
||||
end
|
||||
|
||||
test "follow takes a user and another user" do
|
||||
{ :ok, user } = UserBuilder.insert
|
||||
{ :ok, following } = UserBuilder.insert(%{nickname: "guy"})
|
||||
user = insert(:user)
|
||||
followed = insert(:user)
|
||||
|
||||
{:ok, user } = User.follow(user, following)
|
||||
{:ok, user } = User.follow(user, followed)
|
||||
|
||||
user = Repo.get(User, user.id)
|
||||
|
||||
assert user.following == [User.ap_followers(following)]
|
||||
assert user.following == [User.ap_followers(followed)]
|
||||
end
|
||||
|
||||
test "unfollow takes a user and another user" do
|
||||
{ :ok, following } = UserBuilder.insert(%{nickname: "guy"})
|
||||
{ :ok, user } = UserBuilder.insert(%{following: [User.ap_followers(following)]})
|
||||
followed = insert(:user)
|
||||
user = insert(:user, %{following: [User.ap_followers(followed)]})
|
||||
|
||||
{:ok, user } = User.unfollow(user, following)
|
||||
{:ok, user } = User.unfollow(user, followed)
|
||||
|
||||
user = Repo.get(User, user.id)
|
||||
|
||||
@ -47,8 +49,8 @@ defmodule Pleroma.UserTest do
|
||||
end
|
||||
|
||||
test "test if a user is following another user" do
|
||||
{ :ok, followed } = UserBuilder.insert(%{nickname: "guy"})
|
||||
{ :ok, user } = UserBuilder.insert(%{following: [User.ap_followers(followed)]})
|
||||
followed = insert(:user)
|
||||
user = insert(:user, %{following: [User.ap_followers(followed)]})
|
||||
|
||||
assert User.following?(user, followed)
|
||||
refute User.following?(followed, user)
|
||||
|
File diff suppressed because one or more lines are too long
@ -45,8 +45,11 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenterTest do
|
||||
|
||||
test "an activity" do
|
||||
{:ok, user} = UserBuilder.insert
|
||||
{:ok, mentioned_user } = UserBuilder.insert(%{nickname: "shp", ap_id: "shp"})
|
||||
{:ok, follower} = UserBuilder.insert(%{following: [User.ap_followers(user)]})
|
||||
# {:ok, mentioned_user } = UserBuilder.insert(%{nickname: "shp", ap_id: "shp"})
|
||||
mentioned_user = insert(:user, %{nickname: "shp"})
|
||||
|
||||
# {:ok, follower} = UserBuilder.insert(%{following: [User.ap_followers(user)]})
|
||||
follower = insert(:user, %{following: [User.ap_followers(user)]})
|
||||
|
||||
object = %Object{
|
||||
data: %{
|
||||
@ -62,7 +65,7 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenterTest do
|
||||
}
|
||||
}
|
||||
|
||||
content_html = "Some content mentioning <a href='shp'>@shp</shp>"
|
||||
content_html = "Some content mentioning <a href='#{mentioned_user.ap_id}'>@shp</shp>"
|
||||
content = HtmlSanitizeEx.strip_tags(content_html)
|
||||
date = DateTime.from_naive!(~N[2016-05-24 13:26:08.003], "Etc/UTC") |> DateTime.to_iso8601
|
||||
|
||||
|
@ -5,13 +5,23 @@ defmodule Pleroma.Web.TwitterAPI.Representers.UserRepresenterTest do
|
||||
alias Pleroma.Web.TwitterAPI.Representers.UserRepresenter
|
||||
alias Pleroma.Builders.UserBuilder
|
||||
|
||||
import Pleroma.Factory
|
||||
|
||||
setup do
|
||||
{:ok, user} = UserBuilder.insert
|
||||
user = insert(:user)
|
||||
[user: user]
|
||||
end
|
||||
|
||||
test "A user with an avatar object", %{user: user} do
|
||||
image = "image"
|
||||
user = %{ user | avatar: %{ "url" => [%{"href" => image}] }}
|
||||
represented = UserRepresenter.to_map(user)
|
||||
assert represented["profile_image_url"] == image
|
||||
end
|
||||
|
||||
test "A user", %{user: user} do
|
||||
image = "https://placehold.it/48x48"
|
||||
|
||||
represented = %{
|
||||
"id" => user.id,
|
||||
"name" => user.name,
|
||||
|
@ -94,10 +94,10 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do
|
||||
end
|
||||
|
||||
test "with credentials", %{conn: conn, user: current_user} do
|
||||
{:ok, user} = UserBuilder.insert
|
||||
user = insert(:user)
|
||||
activities = ActivityBuilder.insert_list(30, %{"to" => [User.ap_followers(user)]}, %{user: user})
|
||||
returned_activities = ActivityBuilder.insert_list(10, %{"to" => [User.ap_followers(user)]}, %{user: user})
|
||||
{:ok, other_user} = UserBuilder.insert(%{ap_id: "glimmung", nickname: "nockame"})
|
||||
other_user = insert(:user)
|
||||
ActivityBuilder.insert_list(10, %{}, %{user: other_user})
|
||||
since_id = List.last(activities).id
|
||||
|
||||
@ -110,7 +110,7 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do
|
||||
response = json_response(conn, 200)
|
||||
|
||||
assert length(response) == 10
|
||||
assert response == Enum.map(returned_activities, fn (activity) -> ActivityRepresenter.to_map(activity, %{user: user, for: current_user}) end)
|
||||
assert response == Enum.map(returned_activities, fn (activity) -> ActivityRepresenter.to_map(activity, %{user: User.get_cached_by_ap_id(activity.data["actor"]), for: current_user}) end)
|
||||
end
|
||||
end
|
||||
|
||||
@ -122,7 +122,7 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do
|
||||
end
|
||||
|
||||
test "with credentials", %{conn: conn, user: current_user} do
|
||||
{:ok, followed } = UserBuilder.insert(%{name: "some guy"})
|
||||
followed = insert(:user)
|
||||
|
||||
conn = conn
|
||||
|> with_credentials(current_user.nickname, "test")
|
||||
@ -142,7 +142,7 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do
|
||||
end
|
||||
|
||||
test "with credentials", %{conn: conn, user: current_user} do
|
||||
{:ok, followed } = UserBuilder.insert(%{name: "some guy"})
|
||||
followed = insert(:user)
|
||||
|
||||
{:ok, current_user} = User.follow(current_user, followed)
|
||||
assert current_user.following == [User.ap_followers(followed)]
|
||||
@ -157,6 +157,24 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST /api/qvitter/update_avatar.json" do
|
||||
setup [:valid_user]
|
||||
test "without valid credentials", %{conn: conn} do
|
||||
conn = post conn, "/api/qvitter/update_avatar.json"
|
||||
assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
|
||||
end
|
||||
|
||||
test "with credentials", %{conn: conn, user: current_user} do
|
||||
conn = conn
|
||||
|> with_credentials(current_user.nickname, "test")
|
||||
|> post("/api/qvitter/update_avatar.json", %{img: Pleroma.Web.ActivityPub.ActivityPubTest.data_uri})
|
||||
|
||||
current_user = Repo.get(User, current_user.id)
|
||||
assert is_map(current_user.avatar)
|
||||
assert json_response(conn, 200) == UserRepresenter.to_map(current_user, %{for: current_user})
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST /api/favorites/create/:id" do
|
||||
setup [:valid_user]
|
||||
test "without valid credentials", %{conn: conn} do
|
||||
|
@ -78,7 +78,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
|
||||
|
||||
test "fetch public statuses" do
|
||||
%{ public: activity, user: user } = ActivityBuilder.public_and_non_public
|
||||
{:ok, follower } = UserBuilder.insert(%{name: "dude", ap_id: "idididid", following: [User.ap_followers(user)]})
|
||||
|
||||
follower = insert(:user, following: [User.ap_followers(user)])
|
||||
|
||||
statuses = TwitterAPI.fetch_public_statuses(follower)
|
||||
|
||||
@ -87,19 +88,18 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
|
||||
end
|
||||
|
||||
test "fetch friends' statuses" do
|
||||
ActivityBuilder.public_and_non_public
|
||||
|
||||
user = insert(:user, %{following: ["someguy/followers"]})
|
||||
{:ok, activity} = ActivityBuilder.insert(%{"to" => ["someguy/followers"]})
|
||||
{:ok, direct_activity} = ActivityBuilder.insert(%{"to" => ["some other id"]})
|
||||
{:ok, user} = UserBuilder.insert(%{ap_id: "some other id", following: ["someguy/followers"]})
|
||||
{:ok, direct_activity} = ActivityBuilder.insert(%{"to" => [user.ap_id]})
|
||||
|
||||
statuses = TwitterAPI.fetch_friend_statuses(user)
|
||||
|
||||
activity_user = Repo.get_by(User, ap_id: activity.data["actor"])
|
||||
direct_activity_user = Repo.get_by(User, ap_id: direct_activity.data["actor"])
|
||||
|
||||
assert length(statuses) == 2
|
||||
assert Enum.at(statuses, 0) == ActivityRepresenter.to_map(activity, %{user: activity_user})
|
||||
assert Enum.at(statuses, 1) == ActivityRepresenter.to_map(direct_activity, %{user: activity_user, mentioned: [user]})
|
||||
assert Enum.at(statuses, 1) == ActivityRepresenter.to_map(direct_activity, %{user: direct_activity_user, mentioned: [user]})
|
||||
end
|
||||
|
||||
test "fetch a single status" do
|
||||
@ -113,8 +113,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
|
||||
end
|
||||
|
||||
test "Follow another user" do
|
||||
{ :ok, user } = UserBuilder.insert
|
||||
{ :ok, following } = UserBuilder.insert(%{nickname: "guy"})
|
||||
user = insert(:user)
|
||||
following = insert(:user)
|
||||
|
||||
{:ok, user, following, activity } = TwitterAPI.follow(user, following.id)
|
||||
|
||||
@ -126,8 +126,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
|
||||
end
|
||||
|
||||
test "Unfollow another user using user_id" do
|
||||
{ :ok, following } = UserBuilder.insert(%{nickname: "guy"})
|
||||
{ :ok, user } = UserBuilder.insert(%{following: [User.ap_followers(following)]})
|
||||
following = insert(:user)
|
||||
user = insert(:user, %{following: [User.ap_followers(following)]})
|
||||
|
||||
{:ok, user, _following } = TwitterAPI.unfollow(user, %{"user_id" => following.id})
|
||||
|
||||
@ -137,8 +137,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
|
||||
end
|
||||
|
||||
test "Unfollow another user using screen_name" do
|
||||
{ :ok, following } = UserBuilder.insert(%{nickname: "guy"})
|
||||
{ :ok, user } = UserBuilder.insert(%{following: [User.ap_followers(following)]})
|
||||
following = insert(:user)
|
||||
user = insert(:user, %{following: [User.ap_followers(following)]})
|
||||
|
||||
{:ok, user, _following } = TwitterAPI.unfollow(user, %{"screen_name" => following.nickname})
|
||||
|
||||
@ -171,8 +171,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
|
||||
test "it can parse mentions and return the relevant users" do
|
||||
text = "@gsimg According to @archaeme , that is @daggsy."
|
||||
|
||||
{:ok, gsimg} = UserBuilder.insert(%{nickname: "gsimg"})
|
||||
{:ok, archaeme} = UserBuilder.insert(%{nickname: "archaeme"})
|
||||
gsimg = insert(:user, %{nickname: "gsimg"})
|
||||
archaeme = insert(:user, %{nickname: "archaeme"})
|
||||
|
||||
expected_result = [
|
||||
{"@gsimg", gsimg},
|
||||
@ -185,11 +185,11 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
|
||||
test "it adds user links to an existing text" do
|
||||
text = "@gsimg According to @archaeme , that is @daggsy."
|
||||
|
||||
{:ok, _gsimg} = UserBuilder.insert(%{nickname: "gsimg", ap_id: "first_link" })
|
||||
{:ok, _archaeme} = UserBuilder.insert(%{nickname: "archaeme", ap_id: "second_link"})
|
||||
gsimg = insert(:user, %{nickname: "gsimg"})
|
||||
archaeme = insert(:user, %{nickname: "archaeme"})
|
||||
|
||||
mentions = TwitterAPI.parse_mentions(text)
|
||||
expected_text = "<a href='first_link'>@gsimg</a> According to <a href='second_link'>@archaeme</a> , that is @daggsy."
|
||||
expected_text = "<a href='#{gsimg.ap_id}'>@gsimg</a> According to <a href='#{archaeme.ap_id}'>@archaeme</a> , that is @daggsy."
|
||||
|
||||
assert TwitterAPI.add_user_links(text, mentions) == expected_text
|
||||
end
|
||||
|
Loading…
Reference in New Issue
Block a user