feat: 播放地址优先使用 playlist 并补充接口清单
This commit is contained in:
+470
@@ -0,0 +1,470 @@
|
|||||||
|
# RaptDrama 接口清单
|
||||||
|
|
||||||
|
基址使用 `https://apis.raptdrama.com`。`https://app.raptdrama.com` 上部分路径也能通,脚本以 `apis` 为准。
|
||||||
|
|
||||||
|
本文只记录已经实际调用过的返回结构。安装包里还有一批路径,返回格式没有核对,放在最后一节,不要把它们当成已确认的协议。
|
||||||
|
|
||||||
|
## 通用约定
|
||||||
|
|
||||||
|
业务响应外面都有同一层包装。HTTP 状态经常仍是 200,成功与否看 `code`。
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "ok",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
已见到的业务码:
|
||||||
|
|
||||||
|
| code | 含义 |
|
||||||
|
| --- | --- |
|
||||||
|
| 200 | 成功。`msg` 可能是 `ok`、`成功` 或 `Login success` |
|
||||||
|
| 299 | 未登录,`msg` 为 `Please login first`。用 `Authorization: Bearer` 调播放接口时会出现 |
|
||||||
|
| 500 | 参数错误,例如 `Invalid email format`、`Password cannot be empty`、`Error Type` |
|
||||||
|
|
||||||
|
请求头:
|
||||||
|
|
||||||
|
| 头 | 值 |
|
||||||
|
| --- | --- |
|
||||||
|
| Content-Type | `application/json`。播放和登录都用 JSON,不要用表单 |
|
||||||
|
| Accept | `application/json` |
|
||||||
|
| User-Agent | `RaptDrama/1.1.81 (Android 12; 23113RKC6C; Redmi)` |
|
||||||
|
| apasstk | 登录后拿到的 `token`。游客 `autologin` 本身不带这个头 |
|
||||||
|
| x-app-build | `139` |
|
||||||
|
| x-app-version | `1.1.81` |
|
||||||
|
| x-device-type | `phone` |
|
||||||
|
| x-device-brand | `Redmi` |
|
||||||
|
| x-device-model | `23113RKC6C` |
|
||||||
|
| x-device-id | 设备号,例如 `rd_` 开头的字符串 |
|
||||||
|
| x-device-fingerprint | 设备指纹 |
|
||||||
|
| x-device-physical | `true` |
|
||||||
|
| x-os-name | `Android` |
|
||||||
|
| x-os-version | `12` |
|
||||||
|
| accept-language | `en-US` |
|
||||||
|
| accept-language-custom | `en-US` |
|
||||||
|
|
||||||
|
登录凭证只放在 `apasstk`。不要再使用 `Authorization: Bearer`。
|
||||||
|
|
||||||
|
## 登录
|
||||||
|
|
||||||
|
### POST /app/open/autologin
|
||||||
|
|
||||||
|
每次调用都新建一个游客,不会沿用上一次的账号。不需要 `apasstk`。
|
||||||
|
|
||||||
|
请求体:`{}`
|
||||||
|
|
||||||
|
`data`:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| id | string | 新游客 id |
|
||||||
|
| nickname | string | 随机昵称 |
|
||||||
|
| token | string | 后续请求的 `apasstk` |
|
||||||
|
| score | int | |
|
||||||
|
| vipday | int | |
|
||||||
|
| badge | string | 例如 `BRONZE` |
|
||||||
|
| watched | int | |
|
||||||
|
| rewards | int | |
|
||||||
|
| avatarNum | int | |
|
||||||
|
| avatarNumUrl | string | 头像地址 |
|
||||||
|
|
||||||
|
### POST /app/open/emailLogin
|
||||||
|
|
||||||
|
同一邮箱重复登录返回同一个用户。不需要先带 `apasstk`。
|
||||||
|
|
||||||
|
请求体:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"email": "邮箱", "password": "密码"}
|
||||||
|
```
|
||||||
|
|
||||||
|
`email` 或 `password` 用错字段名时,`code` 为 500。
|
||||||
|
|
||||||
|
`data`:
|
||||||
|
|
||||||
|
| 字段 | 类型 |
|
||||||
|
| --- | --- |
|
||||||
|
| id | int |
|
||||||
|
| nickname | string |
|
||||||
|
| email | string |
|
||||||
|
| email_bind | int |
|
||||||
|
| score | int |
|
||||||
|
| tgid | int |
|
||||||
|
| isguanzhu | int |
|
||||||
|
| sxid | int |
|
||||||
|
| os | string |
|
||||||
|
| channel | string |
|
||||||
|
| token | string |
|
||||||
|
| google_bind | int |
|
||||||
|
|
||||||
|
`/auth/login` 在这个基址上没有返回上述 JSON 包装,邮箱登录以 `/app/open/emailLogin` 为准。
|
||||||
|
|
||||||
|
### POST /app/user/getUserInfo
|
||||||
|
|
||||||
|
需要 `apasstk`。游客 token 也可以调用。
|
||||||
|
|
||||||
|
请求体:`{}`
|
||||||
|
|
||||||
|
`data`:
|
||||||
|
|
||||||
|
| 字段 | 类型 |
|
||||||
|
| --- | --- |
|
||||||
|
| id | int |
|
||||||
|
| nickname | string |
|
||||||
|
| email_bind | int |
|
||||||
|
| mobile_bind | int |
|
||||||
|
| score | int |
|
||||||
|
| tgid | int |
|
||||||
|
| isguanzhu | int |
|
||||||
|
| sxid | int |
|
||||||
|
| os | string |
|
||||||
|
| channel | string |
|
||||||
|
| google_bind | int |
|
||||||
|
| apple_bind | int |
|
||||||
|
| login_day | int |
|
||||||
|
| vipday | int |
|
||||||
|
| badge | string |
|
||||||
|
| watched | int |
|
||||||
|
| rewards | int |
|
||||||
|
| avatarNum | int |
|
||||||
|
| avatarNumUrl | string |
|
||||||
|
|
||||||
|
这里的 `id` 是数字。`autologin` 返回的 `id` 是字符串。
|
||||||
|
|
||||||
|
## 分类和剧列表
|
||||||
|
|
||||||
|
剧对象在多个接口里重复出现,下面称它为剧目:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| id | int | 剧 id,播放接口的 `vid` 用这个值 |
|
||||||
|
| title | string | |
|
||||||
|
| update_time | int | Unix 时间 |
|
||||||
|
| view | int | |
|
||||||
|
| image | string | 封面 |
|
||||||
|
| tstype | string | 分类 id,Passion 为 `"20"` |
|
||||||
|
| desc | string | |
|
||||||
|
| forstaus | string | |
|
||||||
|
| forstausen | string | 连载状态文案,例如完结 |
|
||||||
|
| author | string | |
|
||||||
|
| classify | string[] | 题材标签 |
|
||||||
|
| score | string | 评分,是字符串 |
|
||||||
|
|
||||||
|
### POST /app/open/classifyv2
|
||||||
|
|
||||||
|
请求体:`{}`
|
||||||
|
|
||||||
|
`data` 是分类数组:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| id | int | 传给分类视频接口的 `type` |
|
||||||
|
| label | string | 英文名 |
|
||||||
|
| label_origin | string | |
|
||||||
|
| image | string 或 null | |
|
||||||
|
|
||||||
|
已见到的分类:`18` Comedy,`20` Passion,`21` Newest,`22` Western,`23` Comic drama。
|
||||||
|
|
||||||
|
### POST /app/open/classify_video
|
||||||
|
|
||||||
|
按分类分页。`type` 才是分类 id。传 `tstype`、`classify` 或 `id` 会返回 `code` 500、`Error Type`。
|
||||||
|
|
||||||
|
请求体:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"page": 1, "limit": 10, "type": 20}
|
||||||
|
```
|
||||||
|
|
||||||
|
`page` 从 1 开始。每页 10 条,空页表示结束。`limit` 增大也不会一次返回超过 10 条。
|
||||||
|
|
||||||
|
`data` 是剧目数组。
|
||||||
|
|
||||||
|
### POST /app/open/recommend
|
||||||
|
|
||||||
|
请求体:`{}`
|
||||||
|
|
||||||
|
`data` 是剧目数组。空对象也只返回约 10 条,不是全站列表。
|
||||||
|
|
||||||
|
### POST /app/open/foryou
|
||||||
|
|
||||||
|
请求体:`{}`
|
||||||
|
|
||||||
|
`data` 是剧目数组,并多一个 `firstchapter`:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| firstchapter.id | int | 集 id |
|
||||||
|
| firstchapter.title | string | |
|
||||||
|
| firstchapter.idx | int | 集序号 |
|
||||||
|
| firstchapter.playurl | string | 这一集的播放地址 |
|
||||||
|
|
||||||
|
### POST /app/open/extra_broadcast
|
||||||
|
|
||||||
|
请求体:`{}`
|
||||||
|
|
||||||
|
`data` 是剧目数组。
|
||||||
|
|
||||||
|
### GET /app/open/orgin
|
||||||
|
|
||||||
|
无请求体。`data` 是剧目数组。
|
||||||
|
|
||||||
|
### POST /app/open/search
|
||||||
|
|
||||||
|
请求体:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"keyword": "love", "page": 1, "limit": 10}
|
||||||
|
```
|
||||||
|
|
||||||
|
`data` 是剧目数组,并多一个 `totalvideo`(int,集数)。
|
||||||
|
|
||||||
|
### POST /app/open/bank
|
||||||
|
|
||||||
|
请求体:`{}`
|
||||||
|
|
||||||
|
`data` 不是剧目数组,而是观看或片库记录:
|
||||||
|
|
||||||
|
| 字段 | 类型 |
|
||||||
|
| --- | --- |
|
||||||
|
| id | int |
|
||||||
|
| vid | int |
|
||||||
|
| cid | int |
|
||||||
|
| create_time | int |
|
||||||
|
| video.id | int |
|
||||||
|
| video.title | string |
|
||||||
|
| video.image | string |
|
||||||
|
| video.desc | string |
|
||||||
|
| video.forstaus | string |
|
||||||
|
| video.author | string |
|
||||||
|
| video.forstausen | string |
|
||||||
|
|
||||||
|
## 剧集和播放地址
|
||||||
|
|
||||||
|
### GET /app/video/getchapters
|
||||||
|
|
||||||
|
查询参数:
|
||||||
|
|
||||||
|
| 参数 | 说明 |
|
||||||
|
| --- | --- |
|
||||||
|
| vid | 剧 id |
|
||||||
|
| start | 起始偏移,从 0 开始 |
|
||||||
|
| limit | 每页条数,可用 200 |
|
||||||
|
|
||||||
|
示例:`/app/video/getchapters?vid=1942&start=0&limit=200`
|
||||||
|
|
||||||
|
`data`:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| count | int | 总集数 |
|
||||||
|
| list[].id | int | 集 id,对应播放列表里的 `cid` |
|
||||||
|
| list[].title | string | |
|
||||||
|
| list[].idx | int | 集序号 |
|
||||||
|
| list[].isvip | string | `"0"` 免费,`"1"` 锁定。这是字符串,不是布尔值 |
|
||||||
|
|
||||||
|
### POST /app/video/playlist
|
||||||
|
|
||||||
|
安装包里没有这个路径,但服务端仍接受,GET 和 POST 都可以。请求体与 `playlistv2` 相同。
|
||||||
|
|
||||||
|
`data` 的字段和 `playlistv2` 一样。区别是它一次返回该剧全部集,不再按每页 10 条分页。剧 1942 一次返回 `count` 30、`list` 30 条,其中免费的 3 集有 `src`,其余为空。第一集的 `src` 与 `playlistv2` 第一页相同。脚本优先调用这个接口,结果为空、条数少于 `count` 或请求失败时,再按页调用 `playlistv2`。
|
||||||
|
|
||||||
|
`playlistv1` 和 `playlistv3` 返回 nginx 404。
|
||||||
|
|
||||||
|
### POST /app/video/playlistv2
|
||||||
|
|
||||||
|
这是播放地址接口。`vid` 是剧 id,不是集 id。
|
||||||
|
|
||||||
|
请求体:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"vid": 1942, "page": 1, "before": 1}
|
||||||
|
```
|
||||||
|
|
||||||
|
第一页 `before` 为 `1`,后面的页为 `0`。每页 10 集。`data.count` 到齐后停止。
|
||||||
|
|
||||||
|
`data`:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| count | int | 总集数 |
|
||||||
|
| list[].cid | int | 集 id |
|
||||||
|
| list[].idx | int | 集序号 |
|
||||||
|
| list[].msg | string | 例如 `EP.1` |
|
||||||
|
| list[].src | string | 主 m3u8。未解锁时是空字符串 |
|
||||||
|
| list[].state | string | 例如 `pause` |
|
||||||
|
| list[].like | int | |
|
||||||
|
| list[].like_n | int | |
|
||||||
|
| list[].playNumber | int | |
|
||||||
|
| list[].fufei | int | |
|
||||||
|
| list[].playIng | bool | |
|
||||||
|
| list[].isShowimage | bool | |
|
||||||
|
| list[].needScore | string | |
|
||||||
|
| list[].isplay | bool | |
|
||||||
|
|
||||||
|
游客和未购买账号只能拿到免费集的 `src`。锁定集的 `src` 为空,接口不会因为换了登录方式就返回地址。
|
||||||
|
|
||||||
|
`src` 是 BunnyCDN 签名地址,形如:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://vz-<编号>.b-cdn.net/bcdn_token=<签名>&token_path=%2F<视频uuid>%2F&expires=<unix时间>/<视频uuid>/playlist.m3u8
|
||||||
|
```
|
||||||
|
|
||||||
|
签名写在路径里,不是查询字符串。主列表里有一行相对路径 `1080p/video.m3u8`。子列表是点播,分片名为 `video0.ts`、`video1.ts`。同一段签名前缀可以访问该视频目录下的子列表和 ts。签名会过期,需要时重新请求 `playlistv2`。
|
||||||
|
|
||||||
|
### POST /app/video/videoinfo
|
||||||
|
|
||||||
|
请求体:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"vid": 1942}
|
||||||
|
```
|
||||||
|
|
||||||
|
`data`:
|
||||||
|
|
||||||
|
| 字段 | 类型 |
|
||||||
|
| --- | --- |
|
||||||
|
| moshi | int |
|
||||||
|
| video.id | int |
|
||||||
|
| video.title | string |
|
||||||
|
| video.view | int |
|
||||||
|
| video.desc | string |
|
||||||
|
| video.ishot | string |
|
||||||
|
| video.like | int |
|
||||||
|
| video.is_on | int |
|
||||||
|
| video.poster | string |
|
||||||
|
| video.forstaus | string |
|
||||||
|
| video.author | string |
|
||||||
|
| video.forstausen | string |
|
||||||
|
|
||||||
|
带已登录账号时,`data` 里还出现过 `history`。
|
||||||
|
|
||||||
|
### POST /app/video/addreadlog
|
||||||
|
|
||||||
|
App 播放时发出的请求体如下,返回格式没有单独保存:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"vid": 2286, "del": 0, "cid": 26125}
|
||||||
|
```
|
||||||
|
|
||||||
|
`vid` 是剧 id,`cid` 是集 id。
|
||||||
|
|
||||||
|
## 其他已核对接口
|
||||||
|
|
||||||
|
### POST /app/open/contact
|
||||||
|
|
||||||
|
请求体:`{}`
|
||||||
|
|
||||||
|
`data`:
|
||||||
|
|
||||||
|
| 字段 | 类型 |
|
||||||
|
| --- | --- |
|
||||||
|
| techniacl_email | string |
|
||||||
|
| business_email | string |
|
||||||
|
|
||||||
|
字段名在服务端就是 `techniacl_email`。
|
||||||
|
|
||||||
|
### POST /app/appopen/getAuthConfig
|
||||||
|
|
||||||
|
请求体:`{}`
|
||||||
|
|
||||||
|
`data`:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| platform | string | |
|
||||||
|
| google.enabled | bool | |
|
||||||
|
| google.android_client_id | string | |
|
||||||
|
| google.ios_client_id | string | |
|
||||||
|
| google.web_client_id | string | |
|
||||||
|
| google.client_id | string | |
|
||||||
|
| google.server_client_id | string | |
|
||||||
|
| apple.enabled | bool | |
|
||||||
|
| apple.service_id | string | |
|
||||||
|
| apple.team_id | string | |
|
||||||
|
| apple.key_id | string | |
|
||||||
|
| supported_platforms | string[] | |
|
||||||
|
|
||||||
|
## 安装包中的路径
|
||||||
|
|
||||||
|
这些路径从 `libapp.so` 提取。除上面已写明的接口外,访问方式和返回格式都还没有核对。
|
||||||
|
|
||||||
|
认证:
|
||||||
|
|
||||||
|
- POST /app/user/appleLogin
|
||||||
|
- POST /app/user/googleLogin
|
||||||
|
- POST /app/user/bindEmail
|
||||||
|
- POST /auth/register
|
||||||
|
- POST /auth/refresh
|
||||||
|
- POST /auth/bind-email
|
||||||
|
|
||||||
|
用户:
|
||||||
|
|
||||||
|
- /app/user/accountBindingStatus
|
||||||
|
- /app/user/editProfile
|
||||||
|
- /app/user/changePassword
|
||||||
|
- /app/user/deleteAccount
|
||||||
|
- /app/user/activate
|
||||||
|
- /app/user/activationSubOverview
|
||||||
|
- /app/user/getPlatformBinds
|
||||||
|
- /app/user/getSensorsProfile
|
||||||
|
- /app/user/recordAttribution
|
||||||
|
- /app/user/readhistory
|
||||||
|
- /app/user/delhistory
|
||||||
|
|
||||||
|
视频:
|
||||||
|
|
||||||
|
- /app/video/overview
|
||||||
|
- /app/video/addToWishlist
|
||||||
|
- /app/video/markvideo
|
||||||
|
- /app/video/exitRecommendm
|
||||||
|
- /app/video/screening
|
||||||
|
- /app/User/checkViewTime
|
||||||
|
|
||||||
|
支付和订阅:
|
||||||
|
|
||||||
|
- /app/pay/google_iap_create_order
|
||||||
|
- /app/pay/google_iap_verify
|
||||||
|
- /app/pay/google_iap_status
|
||||||
|
- /app/pay/google_iap_recovery
|
||||||
|
- /app/pay/apple_iap_create_order
|
||||||
|
- /app/pay/apple_iap_verify
|
||||||
|
- /app/pay/apple_iap_recovery
|
||||||
|
- /app/pay/paylmpay_create_order
|
||||||
|
- /app/pay/paypal_return
|
||||||
|
- /app/pay/paypal_cancel
|
||||||
|
- /app/pay/paystatus
|
||||||
|
- /app/pay/paysuccess
|
||||||
|
- /app/pay/initiateCheckout
|
||||||
|
- /app/pay/report_payment_panel_context
|
||||||
|
- /app/pay_center/create_order
|
||||||
|
- /app/pay_center/paylist
|
||||||
|
- /app/pay_center/third_party_qualification
|
||||||
|
- /app/user/paylist
|
||||||
|
- /app/user/paylistv2
|
||||||
|
- /app/user/paylistv3
|
||||||
|
- /app/subscription/list
|
||||||
|
- /app/subscription/cancel
|
||||||
|
|
||||||
|
签到、反馈、通知:
|
||||||
|
|
||||||
|
- /app/checkin/config
|
||||||
|
- /app/checkin/status
|
||||||
|
- /app/checkin/perform
|
||||||
|
- /app/checkin/history
|
||||||
|
- /app/checkin/statistics
|
||||||
|
- /app/checkin/tasks
|
||||||
|
- /app/feedback/submit
|
||||||
|
- /app/feedback/list
|
||||||
|
- /app/feedback/detail
|
||||||
|
- /app/feedback/read
|
||||||
|
- /app/feedback/categories
|
||||||
|
- /app/feedback/unreadCount
|
||||||
|
- /app/feedback/upload
|
||||||
|
- /app/notification/getList
|
||||||
|
- /app/notification/getDetail
|
||||||
|
- /app/notification/device
|
||||||
|
- /app/notification/settings
|
||||||
|
- /app/notification/action
|
||||||
|
- /app/notification/clear
|
||||||
+35
-7
@@ -268,7 +268,23 @@ def list_chapters(token, vid):
|
|||||||
return chapters
|
return chapters
|
||||||
|
|
||||||
|
|
||||||
def list_playlist(token, vid):
|
def _playlist_items(data):
|
||||||
|
if isinstance(data, list):
|
||||||
|
return data, len(data)
|
||||||
|
items = (data or {}).get("list") or []
|
||||||
|
total = (data or {}).get("count")
|
||||||
|
return items, total
|
||||||
|
|
||||||
|
|
||||||
|
def _playlist_complete(items, total):
|
||||||
|
if not items:
|
||||||
|
return False
|
||||||
|
if total is None:
|
||||||
|
return True
|
||||||
|
return len(items) >= int(total)
|
||||||
|
|
||||||
|
|
||||||
|
def list_playlist_v2(token, vid):
|
||||||
episodes = []
|
episodes = []
|
||||||
page = 1
|
page = 1
|
||||||
total = None
|
total = None
|
||||||
@@ -278,12 +294,9 @@ def list_playlist(token, vid):
|
|||||||
"page": page,
|
"page": page,
|
||||||
"before": 1 if page == 1 else 0,
|
"before": 1 if page == 1 else 0,
|
||||||
})
|
})
|
||||||
if isinstance(data, list):
|
items, page_total = _playlist_items(data)
|
||||||
items = data
|
if page_total is not None:
|
||||||
total = len(items)
|
total = page_total
|
||||||
else:
|
|
||||||
items = (data or {}).get("list") or []
|
|
||||||
total = (data or {}).get("count", total)
|
|
||||||
if not items:
|
if not items:
|
||||||
break
|
break
|
||||||
episodes.extend(items)
|
episodes.extend(items)
|
||||||
@@ -294,6 +307,21 @@ def list_playlist(token, vid):
|
|||||||
return episodes
|
return episodes
|
||||||
|
|
||||||
|
|
||||||
|
def list_playlist(token, vid):
|
||||||
|
try:
|
||||||
|
data = api("/app/video/playlist", token, "POST", {
|
||||||
|
"vid": vid, "page": 1, "before": 1,
|
||||||
|
})
|
||||||
|
items, total = _playlist_items(data)
|
||||||
|
if _playlist_complete(items, total):
|
||||||
|
return items
|
||||||
|
except Exception as exc:
|
||||||
|
print(f" playlist 失败,改用 playlistv2:{exc}", flush=True)
|
||||||
|
return list_playlist_v2(token, vid)
|
||||||
|
print(f" playlist 结果不完整,改用 playlistv2", flush=True)
|
||||||
|
return list_playlist_v2(token, vid)
|
||||||
|
|
||||||
|
|
||||||
def expand_hls(master_url):
|
def expand_hls(master_url):
|
||||||
"""把主 m3u8 展开成子播放列表和 ts 绝对地址。失败时只保留主地址。"""
|
"""把主 m3u8 展开成子播放列表和 ts 绝对地址。失败时只保留主地址。"""
|
||||||
if not master_url:
|
if not master_url:
|
||||||
|
|||||||
Reference in New Issue
Block a user