先上链接:https://val.bili33.top/

附上可用于参考的文档(在别人的文档上改的,我的文档里附了源文档链接):https://gamernotitle.notion.site/Valorant-API-baffa2069fb848a781664432564e94d0

出现这个Idea是因为最近从Go转瓦去玩了,然后每天商店会刷4个皮肤(玩过瓦的都知道每天8点蹲商店),能实现这个的有很多应用,其中不乏国产的小黑盒、游民星空;Google Play商店上面还有Vshop(因DMCA被暂时下架了),在这些应用中除了Vshop,其他的我用过的都存在一个问题:每天都要重新登陆,就会弄得,很烦。特别是我这种账号开了二次验证的玩家,还要天天跑邮箱收验证码。

然后我就去搜了一下拳头的API文档:Riot Developer Portal (riotgames.com),但是并没有发现相关的API(特别是瓦的API需要的APIkey的权限比较高,个人开发者拳头不给这么高的权限)

拳头的门槛太高了,搞得我被劝退了一段时间,直到我在网上搜索到了这个Notion文档(我的参考文档是从这里改的):新增模块:UAIOSC-valorant;新增功能:每日商店刷新检测等(使用GitHub上从Valorant客户端扒出来的API) (notion.site)

既然有了别人整理出来的文档,那么就着手开始做吧!

我一人就是一支军队哒!!!!!

网站框架

因为个人用Python用的比较多(说白了就是其他的不熟,真不熟吧),所以第一反应是用Flask作为网站的引擎去开发。但是在做后端之前,得先把前端的框架大概搭建一下。为了把网站做的好看一点,我就去找模板去了。

之前做哔哩CDN的时候,用的是Creative TIM的Argon设计语言(creativetimofficial/argon-design-system),这次我本来想在TemplateMonster上面找的,但是这网站上面的模板那叫一个鱼龙混杂呀,所以我还是找回老东家Creative TIM的设计语言,自己去搭建框架,然后看到Soft UI这一套就挺不错的(毕竟总不可能网站都用一个风格嘛),就拿来用了

在大概花了三个小时把网站的前端框架给弄好(第一版只做了首页(登录页)EULA市场页面),然后当天晚上就开始做后端的逻辑

登录部分

登录部分我最开始就想的是用轮子(倒不如说整个网站能用轮子的都用轮子),在Ultronxr大佬的文档中提到了三个登录库(最下面那个是大佬自己改了的,第一个是大佬用Java写的,但是404警告):

我试了一下底下两个(毕竟能不用Java就不用Java嘛),然后发现很尴尬的事情:登录不上。不知道是不是我的打开方式不对,但是我试了半个小时都是不行,那没办法啦,只能另寻他路

我用关键词Riot Auth交友平台上面进行搜索,结果找到了这个仓库:ohlunaaa/Riot-auth (github.com)

我先Clone了下来,然后尝试用里面的example.py进行登录,不管是开了2FA(二步验证,下同)还是没开的都能够登上,并且返回access_tokenentitlement_token,然后我就用它了(这里登录用的我朋友的号,不是我的)

着手把这个轮子改一改,进行亿点点改动,首先登录操作改成一个函数,然后把二步验证也打包成函数,这样输入验证码的时候就调用这个函数就行了,改着改着就给我改成了现在的样子:VSC/RiotLogin.py at master · GamerNoTitle/VSC · GitHub

然后在后端,接收前端传来的form数据,构建了一个登录函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@app.route('/api/login', methods=['POST'])
def RiotLogin():
# print(request.form)
username = request.form.get('Username')
password = request.form.get('Password')
# APServer = request.form.get('APServer')
# EUServer = request.form.get('EUServer')
# NAServer = request.form.get('NAServer')
# KRServer = request.form.get('KRServer')
checked_rule = request.form.get('CheckedRule')
checked_eula = request.form.get('CheckedEULA')
if username == '' or password == '' or not checked_eula or not checked_rule:
return render_template('index.html', infoerror=True)
else:
CREDS = username, password
user = Auth(username, password)
if user.auth:
response = make_response(render_template('myMarket.html'))
response.set_cookie('access_token', user.access_token)
response.set_cookie('entitlement_token', user.entitlement)
response.set_cookie('region', user.Region)
response.set_cookie('username', user.Name)
response.set_cookie('tag', user.Tag)
response.set_cookie('logged', '1')
response.status_code = 200
else:
response = make_response(render_template('index.html', loginerror=True))
return response

你可以发现这里面是有服务器选择的选项接收的,只不过被我注释掉了,其实我一开始以为登录的API需要用户根据自己账号所在地区选择(有的人打美服那就得选NA,同理,打欧服就得选EU,不过国区大部分人是港区、台区、缅甸区啥的,这都属于AP的范围),后来发现在登录的时候会返回地区(见上图),所以就给我注释掉了,后面更是删掉了。

至于二步验证,那是后面干的事情,让我们先进入商店获取阶段

获取商店

获取商店是一个很麻烦的事情,特别是要对武器啥的进行解析,让我们从获取开始

首先根据Ultronxr大佬的文档,有这样的一个API的端点表格(下图)

表格里面写的很详细,我们用到的API为这个(以亚太服为例) -> https://pd.ap.a.pvp.netstore/v2/storefront/

通过初始化一个player对象,来获取玩家的每日商店,并保存到自身的shop变量中,以供后续使用(该版本文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class player:
def __init__(self, access_token: str, entitlement_token: str, region: str, user_id: str):
self.access_token = access_token
self.entitlement = entitlement_token
self.region = region
self.__header = {
'Authorization': f'Bearer {self.access_token}',
'X-Riot-Entitlements-JWT': self.entitlement,
'X-Riot-ClientPlatform': 'ew0KCSJwbGF0Zm9ybVR5cGUiOiAiUEMiLA0KCSJwbGF0Zm9ybU9TIjogIldpbmRvd3MiLA0KCSJwbGF0Zm9ybU9TVmVyc2lvbiI6ICIxMC4wLjE5MDQyLjEuMjU2LjY0Yml0IiwNCgkicGxhdGZvcm1DaGlwc2V0IjogIlVua25vd24iDQp9',
'X-Riot-ClientVersion': 'release-06.07-shipping-16-866145', #requests.get('https://valorant-api.com/v1/version', timeout=30).json()['data']['riotClientVersion'],
'Content-Type': 'application/json'
}
if region == 'ap':
server = apServer
elif region == 'eu':
server = euServer
elif region == 'na':
server = naServer
else:
server = krServer
response = requests.get(f'{server}{store}{user_id}', headers=self.__header, timeout=30)
self.shop = response.json()
if response.status_code == 400 or response.status_code == 404: self.auth = False
else: self.auth = True

从这个API我们能够获取到很详细的商店数据(见文档,这里不贴了,太长了),其中对我们有用的是SkinsPanelLayoutBonusStore这两个东西,分别对应了每日商店(每天4个皮肤)和黑市(赛季结束前20天的商店,里面有6个皮肤)

根据获取到的字典,我们可以提取到皮肤的UUID,例如文档中给的示例紫金狂潮 暴徒的皮肤ID是b9ee2457-481c-6776-3f5b-0ca8e8f90c89,当我使用这个UUID去https://valorant-api.com/查询的时候,根据文档给出的格式https://valorant-api.com/v1/weapons/b9ee2457-481c-6776-3f5b-0ca8e8f90c89访问后发现提示

1
2
3
4
{
"status": 404,
"error": "the requested uuid was not found"
}

然后我就直接访问https://valorant-api.com/v1/weapons/skins,把里面的所有内容扒了下来,做了一个缓存,里面写了所有皮肤名字对应的皮肤UUID(见这里

通过自己做的这个缓存作为索引,去Valorant-API网站上调用到这个皮肤的所有信息,然后进行解析,得到这个皮肤的所有等级和炫彩数据,构成一个对象(下面代码是最初版本,该版本文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class weapon:
def __init__(self, uuid: str, cost: int):
self.uuid = uuid
self.cost = cost
self.weapon_id = None
with open('assets/dict/zh_CN.json') as f:
data = json.loads(f.read())
f.close()
self.name = requests.get(f'https://valorant-api.com/v1/weapons/skinlevels/{self.uuid}?language=zh-CN', timeout=30).json()['data']['displayName']
self.uid = data[self.name] # the real series skin uuid for the weapon, not a level uuid
self.data = requests.get(f'https://valorant-api.com/v1/weapons/skins/{self.uid}?language=zh-CN', timeout=30).json()['data']
self.level = self.data['levels'] # Skin Levels
self.chroma = self.data['chromas'] # Skin Chromas
self.base_img = self.data['displayIcon']

然后直接在主程序里面创建weapon对象的变量,直接调用对象中的数据(该版本文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@app.route('/market', methods=['GET'])
def market():
cookie = request.cookies
access_token = cookie.get('access_token')
entitlement = cookie.get('entitlement_token')
region = cookie.get('region')
userid = cookie.get('user_id')
user = player(access_token, entitlement, region, userid)
if user.auth:
shop = user.shop['SkinsPanelLayout'] # Flite the daily skin
weapon0 = weapon(shop['SingleItemStoreOffers'][0]['OfferID'], shop['SingleItemStoreOffers'][0]["Cost"]["85ad13f7-3d1b-5128-9eb2-7cd8ee0b5741"])
weapon1 = weapon(shop['SingleItemStoreOffers'][1]['OfferID'], shop['SingleItemStoreOffers'][1]["Cost"]["85ad13f7-3d1b-5128-9eb2-7cd8ee0b5741"])
weapon2 = weapon(shop['SingleItemStoreOffers'][2]['OfferID'], shop['SingleItemStoreOffers'][2]["Cost"]["85ad13f7-3d1b-5128-9eb2-7cd8ee0b5741"])
weapon3 = weapon(shop['SingleItemStoreOffers'][3]['OfferID'], shop['SingleItemStoreOffers'][3]["Cost"]["85ad13f7-3d1b-5128-9eb2-7cd8ee0b5741"])
return render_template('myMarket.html', market=True, weapon0={"name": weapon0.name, "cost": weapon0.cost, "img": weapon0.base_img},
weapon1={"name": weapon1.name, "cost": weapon1.cost, "img": weapon1.base_img},
weapon2={"name": weapon2.name, "cost": weapon2.cost, "img": weapon2.base_img},
weapon3={"name": weapon3.name, "cost": weapon3.cost, "img": weapon3.base_img})
else:
response = make_response(redirect('/', 302))
for cookie in request.cookies:
response.delete_cookie(cookie)
return response

接着再把所有数据返回给jinja2进行渲染,把东西填入表格,然后我就写了好大一堆render_template

因为刚好做这个项目的时候夜市是开着的,所以我也把夜市部分给做完了

1
2
3
4
return render_template('myMarket.html', market=True, weapon0={"name": weapon0.name, "cost": weapon0.cost, "img": weapon0.base_img}, 
weapon1={"name": weapon1.name, "cost": weapon1.cost, "img": weapon1.base_img},
weapon2={"name": weapon2.name, "cost": weapon2.cost, "img": weapon2.base_img},
weapon3={"name": weapon3.name, "cost": weapon3.cost, "img": weapon3.base_img})

皮肤缓存

这个缓存不是传统意义上的缓存,是我把所有的皮肤数据扒下来存到文件里当缓存,每30分钟更新一次

我是开了一个新的线程去做的这个东西(下面为更新缓存的函数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import requests
import json
import time

sc_link = 'https://valorant-api.com/v1/weapons/skins?language=zh-CN'
tc_link = 'https://valorant-api.com/v1/weapons/skins?language=zh-TW'
jp_link = 'https://valorant-api.com/v1/weapons/skins?language=ja-JP'
en_link = 'https://valorant-api.com/v1/weapons/skins'

Linkmap = [
('zh-CN', sc_link),
('zh-TW', tc_link),
('ja-JP', jp_link),
('en', en_link)
]

def updateCache():
while True:
print('Updating Cache...')
for lang, link in Linkmap:
res = requests.get(link, timeout=30)

dt = {}
for i in res.json()['data']:
dt[i['displayName']] = i['uuid']

with open(f'assets/dict/{lang}.json', 'wt', encoding='utf8') as f:
f.write(json.dumps(dt))

del res, dt # Free RAM
time.sleep(3600) # refresh cache every 1 hr

if __name__ == '__main__':
updateCache()

下面是我在主程序里面调用新线程

1
2
3
4
import _thread
from utils.Cache import updateCache

_thread.start_new_thread(updateCache, ())

启动信号我是放在flask服务器启动之前,主函数里面,这样就可以启动这个线程,并且每30分钟自动更新一次皮肤的数据缓存

更好的图片预览

在弄完电脑端的页面后,我发现图片太小了,而且帮我测试的同志(@Vanilluv)给我提出了这个建议

我估计他说的是这种(图示为Pixiv的一个浏览器插件 PixivBatchDownloader

但是但是,我实在想不出什么库可以达到这个效果,我就想到我的Blog的主题hexo-theme-butterfly用的fancybox,可以做到点击查看大图的效果,所以我就想做成这种

这个时候,我就开始求助ChatGPT了(因为懒得写html了,写起来真的很烦)

直接按照它给我的做法,往里面写,不一会就做好了√

二步验证

上面说到,一开始的登录部分是不支持二步验证的,但是轮子本身是支持二步验证的。我自己在测试的过程中,一开始如果是点击登录后,然后反弹回去登录页面加多一个验证码框框,让用户输入验证码后再点登陆,发现这样会导致会话终结,也就是说这个时候在拳头那里已经算是一个新的会话了。于是我又去问ChatGPT

Q: 我想在flask服务器中保存requests的session,该怎么做

A: 在 Flask 服务器中,您可以使用 Flask-Session 扩展来保存 requests 的 session。Flask-Session 提供了一个简单的方法来将 Flask 应用程序连接到服务器的会话存储。

诶,这刚好给了我方向,而且它也给了我代码,我把它给我的代码放进去,小改一下登录部分,不一会就做好了,但是下面坑就来了(等会说,记住GPT给我的建议,下面要考)

手机端页面适配

关于手机和电脑的判断,我是通过UA进行判断的

电脑端的UI是像下面这样(Edge电脑端)

1
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.35

而手机的像是下面这样(Edge移动端)

1
Mozilla/5.0 (Linux; Android 13; M2012K11AC) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Mobile Safari/537.36 EdgA/112.0.1722.59

很明显里面有系统的这个字段,手机会有Android字样,虽然不知道iPhone是咋样的,但是把iPhone字段加入判断准没错,这就是我手机和电脑端的判断依据

我一开始的想法是说把表格弄成竖直的样式,就是一个表格,从上到下是名称图片价格这样的顺序,这样想了以后,发现表头是不必要的东西,你想嘛,手机的宽度就那么小,再弄个表头在左边不是显得很多余嘛

然后我又去求助《万能的》GPT,GPT跟我说Soft UI这一套是用Bootstrap5制成的,里面有card这一class(就是我们平常见到的卡片式),然后我就直接把我的代码给GPT,它给我生成了card的代码,直接贴进去,SO EASY~

第一次踩坑:flask新版本

刚好做手机端适配的那一天我跟我爸出去了,所以我是抱着我的苏菲在外面写的代码,好不容易在codespaces里面调试完成了,git push一下,结果就直接BOOM了,提示如下

1
2
3
4
5
6
7
8
9
10
11
127.0.0.1 - - [10/May/2023 16:55:53] "GET / HTTP/1.1" 500 -
Error on request:
Traceback (most recent call last):
File "C:\Users\GamerNoTitle\AppData\Local\Programs\Python\Python310\lib\site-packages\flask\app.py", line 2189, in wsgi_app
ctx.push()
File "C:\Users\GamerNoTitle\AppData\Local\Programs\Python\Python310\lib\site-packages\flask\ctx.py", line 377, in push

self.session = session_interface.open_session(self.app, self.request)
File "C:\Users\GamerNoTitle\AppData\Local\Programs\Python\Python310\lib\site-packages\flask_session\sessions.py", line 329, in open_session
sid = request.cookies.get(app.session_cookie_name)
AttributeError: 'Flask' object has no attribute 'session_cookie_name'

这一个提示直接给我干懵了,因为我在codespaces里面是调试成功的,就没遇到500的问题,结果部署就出了问题,最最最关键的事情是,我当时没有分masterdev分支,也没有分productiondev分支(指部署上),然后直接让我的服务炸裂,不管谁访问都是500。不过当时我也没有发出去,用的人也就我群里的老朋友们,然后我就在群里发了条消息

这个时候刚好准备回家了,我就收拾了一下东西,先回家去

回到家里,我做的第一件事情就是回滚版本(就是把手机端页面适配先去掉),结果还是给我报一样的错误

接着我去问了ChatGPT(这个时候我就要划掉万能这两个字了,完全就是在胡说嘛)

我就很懵逼,我明明是配置了SECRET_KEY的,怎么还出现这种问题呢,我以为是PaaS平台(Zeabur)没有读入我的环境变量,我当时是这么写的

1
app.config['SECRET_KEY'] = 'A7C55959-3577-5F44-44B6-11540853E272' if not os.environ.get('SECRET_KEY') else os.environ.get('SECRET_KEY')

这里很明显就是说没有读取到SECRET_KEY也有一个缺省值,这样写了以后还是死活打不开,我甚至去问了Zeabur的管理员(真的要给他点赞他真的每次都在解答群友的问题)

就是说我写法是没问题的,而且我后来开了个简单的应用试了一下,代码是这样的

实测这个变量env确实进去了,不过端口没有进去是因为它用了Gunicorn开服务,所以端口不是在这里设置的

所以说,不是我代码的问题,应该是其他的原因

我重新开了一个Codespaces(为了重新开一个环境),然后按照我之前的配置方法去配置,诶,也出现了这个问题

这更加印证了代码不是这个问题的锅,应该是其他的原因

后来我去翻flask的文档和flask-session的文档,结果在flask的文档里面找到了这个更新 -> Update 2.3.0

注意我选定的位置,没错flask 2.3.0更新把session_cookie_name给删掉了,且Flask-Session没有对此进行适配,导致了这个错误

于是我赶紧在requirements.txt里面固定了flask的版本

1
flask==2.2.4

重新部署后发现没问题了,这才安心了

从此刻开始,我开了Dev分支,在Zeabur也设置了Dev环境,用来开发用(省得又bug了导致服务中断)

中场休息

嗯没错,崩铁开服了,于是玩了一天的崩铁(开服冲级嘛),但我没想到8点就开服了,我是8点多准备开鼠标连点器抢UID的,因为当天九点半我要学车,月底要考科目二,结果本来想定位那个开服提示的确定键的,自己先点击了一下,然后进去了(捂脸),就变成UID前排了(100头,UID100201759,图为2023/5/10截的,崩铁我是手柄玩家)

更换皮肤的显示语言

这个建议是@Vanilluv提出的,因为这个网站毕竟是看国际服的,又不是看国服的,国际服最多是繁中而不是简中,所以推荐更换为繁中。这个也简单,更改了一下请求API的参数和访问缓存的语言就搞定了

自动保活

就是字面意思啦,上面也提到了拳头给的access_tokenentitlement_token只有一小时的寿命,如果不进行重新获取的话,就会需要重新登陆

但其实这一节做了我很久,因为在我的登录模块的轮子里面有这样的内容

1
data2 = {"language": "en_US","password": self.password,"remember": "true","type": "auth","username": self.username,}

这是登录的POST请求体,我天真地以为直接把remember设置为true就可以了,但是经过两天的测试发现这东西基本就是摆设……

本来我是不报什么希望了,然后在我翻Vshop(因为Vshop有自动更新token的机制,所以去翻了一下看看能不能找到什么线索)的时候,找到了它的官网,在它的Credit页面,有一项引起了我的注意

因为我一开始在Github上没找到什么好的文档,找到的API就是他这里面写的这个valorant-api.com,而且我也用上了,但是这个库我是真的没有看见过,回到家我就直接查看这个库,果不其然里面有我们需要的东西

在文档里面有一项叫做Cookie Reauth,就是利用Cookie进行重新认证

这里的cookie按照我的理解是登陆时用的cookie,我先用浏览器进行访问,直接GET这个链接

https://auth.riotgames.com/authorize?redirect_uri=https%3A%2F%2Fplayvalorant.com%2Fopt_in&client_id=play-valorant-web-prod&response_type=token%20id_token&nonce=1

然后发现它会重定向到一个404页面(这404真好看哪天我要给他扒下来),但是但是,网址栏里面有我们需要的东西

完整链接如下(账号已登出,现在是无效的)

https://playvalorant.com/opt_in#access_token=eyJraWQiOiJzMSIsImFsZyI6IlJTMjU2In0.eyJwcCI6eyJjIjoiYXMifSwic3ViIjoiZjYyMTYyNjUtN2U3NS01NDRjLTgyMGYtZWI5ZGY0MjEyM2UyIiwic2NwIjpbIm9wZW5pZCLCJjbG0iOlsib3BlbmlkIl0sImRhdCI6eyJjIjoidXcxIiwibGlkIjoiV3BvUUhWRW1yY1hmVFJnMVU3MnpwZyJ9LCJpc3MiOiJodHRwczpcL1wvYXV0aC5yaW90Z2FtZXMuY29tIiwiZXhwIjoxNjgzNzE1MTg4LCJpYXQiOjE2ODM3MTE1ODgsImp0aSI6ImRJNzB4d01JMXE4IiwiY2lkIjoicGxheS12YWxvcmFudC13ZWItcHJvZCJ9.ZffyYoYQlaWAOyr3rBSjtqHe4XBa8zlJU2lRvZGA-wgqU5PLR_wrWvd-6kObVwkJzfen7rpcSSQG9CFbZqflBYVtowadeufGarMM9NgRR6Pkyyfuo845M1NJp67O4EkpP0U-hRDrltghETxJLwGYFQntNVM1WWtW19iIZTQeWKk&scope=openid&iss=https%3A%2F%2Fauth.riotgames.com&id_token=eyJraWQiOiJzMSIsInR56ImlkX3Rva2VuK2p3dCIsImFsZyI6IlJTMjU2In0.eyJhdF9oYXNoIjoiZGd3TzM2cVpNdFpsa1ctWm9xdGpZZyIsInN1YiI6ImY2MjE2MjY1LTdlNzUtNTQ0Yy04MjBmLWViOWRmNDIxMjNlMiIsImF1ZCI6InBsYXktdmFsb3JhbnQtd2ViLXByb2QiLCJhY3IiOiIwIiwiYW1yIjpbInBhc3N3b3JkIiwibWZhIiwiY29va2llIl0sImlzcyI6Imh0dHBzOlwvXC9hdXRoLnJpb3RnYW1lcy5jb20iLCJleHAiOjE2ODM3OTc5ODgsImxvY2FsZSI6InpoX01ZIiwiaWF0IjoxNjgzNzExNTg4LCJub25jZSI6IjEifQ.AOQt3i6xEZyQlNKXPT1ds4Lt8sVsEXR3dS7DJ9S8xbNSR1Pd4YON8nDAmV4F_XSH5t9VmBzv54-HLzJvRhJkJAgkOgJQAcHyetgcf0t6MealgfH2HsSnt8w9IlEgJXK6DVwGUA52inZtlq6xQUtfqigNlkXcRFtQQwnt_D-x_TU&token_type=Bearer&session_state=ps8t9j9WtxNYxhSuRy1SHt3aiUmROifqVwiC_zG5k.Nszsu7v9Q35PH87EzTqh8A&expires_in=3600

我们进行拆分,可以分为一下这几个东西

  • access_token:认证用的token
  • scope=openid
  • iss=https%3A%2F%2Fauth.riotgames.com
  • id_token:不知道是个啥token,但我们不用
  • token_type=Bearer
  • session_state:应该是session的标识符
  • expires_in=3600(过期时间3600秒,这也就是1小时寿命的来源)

现在是有了access_token,我们还需要获得entitlement_token才能凑齐七颗龙珠(bushi)

在上面提到的那个文档中,还有这样的一个项目

这个给我们展示了如何获取entitlement_token,这里headers是需要把认证用的token填进去的,这就是为什么要先获取access_token,访问后会获得文档给我们展示的json,从中提取entitlement_token就行了

那么最后的问题来了,如果你是用Python直接访问拳头的链接,会被Cloudflare给挡住(因为你即使有Cookie,但也没有会话Session的存在),就像我调试的这样

所以非常有必要把之前的session给保留,然后我就把之前登录用的session存入用户与我的服务之间的Session里面了,方便调用

然后推入Dev分支,Dev环境,调试了几天(那几天还是51放假),没问题后并入了master分支

皮肤预览

做这个的原因是,既然Vshop有这个功能,那我肯定也要嘛(别人有的我们也得有,毕竟本来想法就是对标Vshop的)

因为我对JavaScript不熟,所以一开始是打算点击按钮以后打开一个新的页面,里面只有一个视频的,结果被@Vanilluv狠狠地吐槽了说不好看

最后还是选择了弹窗,一开始我直接把Soft UI的实例弹窗加进去,发现它不弹弹窗,就,什么也没有

后来我又找GPT去了,他告诉我可能是没有引入js,于是我就去cdnjs里面,找了bootstrap5的js引入后解决了问题,但是新的坑又来了

第二次踩坑:ID选择器变量

当我做好了以后,我发现一个问题就是,有的皮肤等级/炫彩点击就能弹出框框,有的就弹不出来,然后给我控制台报错

我一开始去问GPT,他说我可能是不存在id为这个东西的元素,我就奇了怪了,我说我命名有这东西,而且在F12控制台里面是能够找到对应的元素的,咋就不行

然后我改成在class里面写uuid,选择器改成了.uuid这样的格式,也是跟我说不行,我就非常纳闷

既然GPT给不了我答案,我就开始搜索,搜索着发现有一篇选择器说明的文章(找不到了,要不然我就附上链接了),里面写了选择器的格式规定

1
2
3
4
5
6
7
8
允许的字符:
- 0-9 数字
- A-Z 大写字母
- a-z 小写字母
- 中横线(-)
- 下划线(_)

特别规定:不能以数字开头

我再反过来看,我的这个不合法的选择器正好是以数字开头的,于是我采取了一个办法就是,在武器等级的前面加上weapon-头,在炫彩的前面加上chorma-头,这样才能够正常弹出我需要的框框

多语言

自大部分功能已经做完后,我就开始考虑多语言的事情了,因为有可能不只是我们在用,老外说不定看到了也想试一试(想Peach了),所以就开始做多语言了

因为json写起来很麻烦,而且不能够写注释,于是我用了yaml文档作为我的翻译索引文件的格式

我先按照我的网站,哪些地方的字是可以写其他语言的,大概构建了一下我的yaml文档格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
global:
title:
dailyshop: 每日商店
nightmarket: 折扣夜市
shopdefault: 我的商店
description:
line1: 瓦罗兰特(无畏契约)商店查看,让你不用登陆游戏也能够查看每日商店(仅支持国际服)
line2: 所有的登录名和密码服务器均不进行存取,仅用于登录
nav:
title: 瓦罗兰特商店查看
button:
dailyshop: 每日商店
nightmarket: 折扣夜市
account: 账户管理
authinfo: 认证信息
sponsor: 恰饭链接
logout: 退出登录

login:
front:
title: 登陆你的Riot账号
description:
line1: 服务器不对账号的登录名和密码进行存取
line2: 支持带有二步验证的账号,欢迎注重安全的各位使用!
form:
username:
title: 用户名
placeholder: 请输入你的用户名
password:
title: 密码
placeholder: 请输入密码
checkbox:
remember: 保持登录(减少登录次数)
nostorage: 我已明白网站仅会将我的登录名和密码用于登录我的账号
eula: 我同意<a href="/EULA" class="text-dark"><u>最终用户许可协议</u></a>
button:
login: 登录
alert:
nocheck: 请先正确输入用户名和密码,并勾选下面两项后再点登陆!
loginerror: 登录出错,请检查账号密码,然后重试!

mfa:
front:
title: 二步验证
description:
line1: 请前往你的绑定邮箱获取验证码后,在这里填入
line2: 服务器不对账号的登录名和密码进行存取
form:
code:
title: 二步验证码
placeholder: 请输入验证码
button:
confirm: 确认

market:
welcome:
# Welcome message format:
# <opening>{{player.name}}#{{player.tag}}
# <credit> {{player.vp}} VP & {{player.rp}} RP
opening: 欢迎回来
credit: 你现在持有
title: 今日商店 | Daily Shop
table:
skinname: 皮肤名称
skinimg: 皮肤图片
skincost: 皮肤售价
skinpreview: 皮肤预览
modal:
videonotavaliable: 当前浏览器无法预览视频
button:
close: 关闭

nightmarket:
welcome:
# Welcome message format:
# <opening>{{player.name}}#{{player.tag}}
# <credit> {{player.vp}} VP & {{player.rp}} RP
opening: 欢迎回来
credit: 你现在持有
notavaliable: 夜市还没有开放哦,先去看看每日商店吧!
title: 夜市 | Night Market
table:
skinname: 皮肤名称
skinimg: 皮肤图片
skincost: 皮肤售价
skinpreview: 皮肤预览
modal:
videonotavaliable: 当前浏览器无法预览视频
button:
close: 关闭

error:
error500:
tip: 500服务器错误
error: 肥肠抱歉,服务器发生了点错误
detail:
front: 服务器发生了以下错误:
solve: 你可以尝试点击下方的“重置网站数据”按钮来解决这个问题。<br>如果你已经点击了下面的按钮,但仍然出现了这个问题,请尝试清除浏览器的cookie来重置会话<br>如果上面这两种方法你都已经尝试过了,那么请带着此错误信息联系开发者!
button:
reset: 重置网站数据
error404:
tip: 404未找到
error: 你来到了一个无人所知的地方
button:
home: 返回首页

然后就开始在模板里面修改,因为yaml的读取是用这样的代码

1
2
3
import yaml

lang = yaml.load(var, Loader=yaml.FullLoader)

我一开始以为这个var可以是文件,结果发现读出来的lang的变量类型是str而不是dict,才发现应该读入文件

但是,每次加载页面的时候都要读入文件会导致读写吞吐量变大,而且万一前一个进程还没有释放文件,下一个进程又开始读取了就会出bug(特别是访问量很多的情况下),为了避免这个情况我就采用了linux自带的命令cat来完成这个事情

最后改成了这样子

1
lang=yaml.load(os.popen(f'cat lang/{str(request.accept_languages.best_match(app.config["BABEL_LANGUAGES"]))}.yml').read(), Loader=yaml.FullLoader)

虽然读写可能没有什么改善,但是至少不会锁文件,而且也不会爆内存(指读取后不释放变量)

然后写了几个语言的配置文件,分别是英语en、简中zh-CN、繁中zh-TW、日语(机翻)ja-JP,然后写了对应的yml文件放入lang文件夹

在上面我们做了皮肤预览的按钮,但是有些皮肤的等级会带有特殊的说明,例如

  • 黑市 暴徒 等級2:此造型設計會因你是攻擊方或守備方而變化
  • 靈爭鬪魂 幻象 等級2:每次擊殺敵人時,都會播放專屬視覺特效及音效
  • 781-A協定 幻象 等級5:在地化語音可能會因地區而異
  • 2021冠軍賽 暴徒 等級4(在對戰中取得最多擊殺時才會顯示光環)

这种写在武器等级后面的说明型的文字,如果写进按钮里面会非常的长。本来不做多语言的话就可以直接简单粗暴replace这些字符就行了,但是做了多语言就不可以这么做了

还有就是,在valorant-api返回的数据中,每个等级升级的内容都是用类似metadata的字符去说明的(下面是没做多语言之前直接写死的转换表)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
levelup_info = {
"EEquippableSkinLevelItem::VFX": '视觉效果',
"EEquippableSkinLevelItem::Animation": '视觉动画',
"EEquippableSkinLevelItem::Finisher": '终结特效',
"EEquippableSkinLevelItem::Voiceover": "本地化语音",
"EEquippableSkinLevelItem::SoundEffects": "音效",
"EEquippableSkinLevelItem::FishAnimation": "鱼缸动画",
"EEquippableSkinLevelItem::KillBanner": "击杀旗帜",
"EEquippableSkinLevelItem::TopFrag": "击杀光环",
"EEquippableSkinLevelItem::KillCounter": "击杀计数器",
"EEquippableSkinLevelItem::InspectAndKill": "击杀特效",
"EEquippableSkinLevelItem::KillEffect": "击杀特效&音效",
"EEquippableSkinLevelItem::AttackerDefenderSwap": "随阵营变色"
}

多语言下,我直接把他们归到了metadata字典里面去,写在对应语言的yaml文件中(下面为简中,但武器显示仍然为繁中,所以置换表为繁中,而转换表为简中)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
metadata:
level:
# This means what word "level" shoule be like in the language, you can find it in every upgradable skin
# example: https://valorant-api.com/v1/weapons/skinlevels/4c8a49bd-4118-9523-6612-4a924651b4a9
level: 等級
EEquippableSkinLevelItem::VFX: 视觉效果
EEquippableSkinLevelItem::Animation: 视觉动画
EEquippableSkinLevelItem::Finisher: 终结特效
EEquippableSkinLevelItem::SoundEffects: 音效
# For Protocol 781-A Level 5 https://valorant-api.com/v1/weapons/skinlevels/a117218e-4f0e-0cca-7758-7ea30b08ac05
EEquippableSkinLevelItem::Voiceover: 本地化语音
# For Neptune Level 2 https://valorant-api.com/v1/weapons/skinlevels/24e39414-4a8e-e800-1242-08bd94b5e3c4
EEquippableSkinLevelItem::FishAnimation: 鱼缸动画
# For Neptune Level 3 https://valorant-api.com/v1/weapons/skinlevels/7b2826b6-4771-7529-b656-b89b9c1d86b6
EEquippableSkinLevelItem::KillBanner: 击杀旗帜
# For Champions Set Level 4 https://valorant-api.com/v1/weapons/skinlevels/4c8a49bd-4118-9523-6612-4a924651b4a9
EEquippableSkinLevelItem::TopFrag: 击杀光环
# For RGX 11z Pro Set Level 5 https://valorant-api.com/v1/weapons/skinlevels/796cf1d2-4893-fee7-3401-beb277c726c8
EEquippableSkinLevelItem::KillCounter: 击杀计数器
# For Champions Set Level 2 https://valorant-api.com/v1/weapons/skinlevels/f96ed262-4280-2363-2542-38b5620bfbb5
EEquippableSkinLevelItem::InspectAndKill: 击杀特效
# For some skins https://valorant-api.com/v1/weapons/skinlevels/bf4489ad-4739-555c-2511-7cbcc503566c
EEquippableSkinLevelItem::KillEffect: 击杀特效&音效
# For Black.Market Set Level 2 https://valorant-api.com/v1/weapons/skinlevels/65c7df10-4a5e-7eaa-dc45-d0a46f147b18
EEquippableSkinLevelItem::AttackerDefenderSwap: 随阵营变色
description:
# All these sources to replace can be found at the links above
# You need to add ?language=<your lang> to check your language
# Available language: ar-AE / de-DE / en-US / es-ES / es-MX / fr-FR / id-ID / it-IT / ja-JP / ko-KR / pl-PL / pt-BR / ru-RU / th-TH / tr-TR / vi-VN / zh-CN / zh-TW
# For China mainland players, all the names of the guns are using zh-TW, cause this program does not support Valorant from Tencent.
# example: https://valorant-api.com/v1/weapons/skinlevels/a117218e-4f0e-0cca-7758-7ea30b08ac05?language=zh-CN
Voiceover: 在地化語音可能會因地區而異
KillEffect: 每次擊殺敵人時,都會播放專屬視覺特效及音效
AttackerDefenderSwap: 此造型設計會因你是攻擊方或守備方而變化
TopFrag: (在對戰中取得最多擊殺時才會顯示光環)

然后在模板文件的需要修改语言的对应位置,写好变量,就达成目的啦!

Redis存储session

每次部署的时候,因为我们使用的是filesystem作为session的存储对象,所以在PaaS平台里面,部署会清除之前的数据,导致用户需要重新登陆。因为Flask-Session是支持Redis作为存储的,而且我自己也用的比较多,所以我就做了一个可以使用Redis存储的功能

直接让用户把配置写在环境变量里面(README里面有写)

1
2
3
4
5
6
7
8
$ export SESSION_TYPE=filesystem|redis  # If you want to use redis u need to set it as redis, and configure the following items
$ export REDIS_URL=<Your Redis URL>
# If your redis url cannot be parsed
$ export REDIS_HOST=<Your Redis Host>
$ export REDIS_PORT=<Your Redis Port>
$ export REDIS_PASSWORD=<Your Redis Password>
# Optional
$ export REDIS_SSL=True # If your redis does not support this, please DO NOT configure it, or this will make your application timeout.

这样就可以在filesystemredis中进行选择,我用了upstash的免费Redis存储,一个月可以用1W条命令,但是但是,在我测试的过程中我发现,就我们群里的几个人用的情况下,一天都能去到300条命令,多的时候甚至去到了500,这算下来一个月根本不够用啊

于是我投向了Zeabur的Redis存储应用

第三次踩坑:Redis的SSL连接

我在Zeabur里面新建了一个Redis数据库后,就准备把数据库连接改过去了,反正原来的Redis里面没什么数据,尽早更换也不用迁移数据,让群友重新登陆一下就行了,不换还好,一换就出事了,直接timeout了,我以为是Zeabur的服务问题(因为它还在试运营阶段,平常确实有点小问题),就去问老板

我们排除了半个小时也没排出来问题,然后我也不好意思打扰人家,我就说明天再看吧,然后我自己又捣鼓了一会,然后突然想到一个很重要的问题:我让用户设置过SSL,而且我写的是数据库支持再开,否则别开,我就想Zeabur的数据库是不是不支持SSL(Upstash是支持的),然后我就关掉了SSL试了一下,果不其然就是SSL的问题

然后我就跟老板汇报了这个问题,这事也就这么结了

皮肤库功能

这个还是@Vanilluv跟我提出的(口头上),那既然有要求咱就做嘛

我最开始是用缓存的json文件来做的皮肤库,做了大概两天,反正做的UI跟手机的卡片那样(因为如果还是表格的话会很奇怪)

先注册两个新的路径

1
2
3
4
5
6
7
@ app.route('/library', methods=["GET"])
def library(page: int = 1):
pass

@ app.route('/library/page/<page>', methods=["GET"])
def lib_handler(page: int = 1):
return library(int(page))

这样可以直接用上上面的那个library函数,避免重复造轮子

就做了大概一个下午就做完了,然后就发现了很多问题

对你没看错,这里面个人喜爱随机选择默认(右下角那个叉叉)都没有被过滤掉,而且最恶心的是这个随机选择每个武器对应一个对象,也就是说有多少个武器就有多少个随机选择,我直接头大~

然后我就随手修了一下,但是只是去掉了随机选择,默认皮肤没去掉(后面会说怎么解决的,这是个坑),我用了过滤器但是就是没去掉,让我很难受

这里的uuid是皮肤的UUID不是武器的UUID,如果你看了上面获取商店那一节的话,你就会知道武器的UUID跟默认皮肤(等级1)的UUID不是一个

结果在我晚上遛狗的时候,我想到了新的解决方法

利用Sqlite数据库作为缓存

我是脑袋里面突然蹦出数据库这个东西的,本来我是不怎么用数据库的(因为没怎么碰过数据库语法),但是想到后面还要做搜索功能,还是弄个数据库吧

于是我现在Navicat里面模拟建立数据库,按照如下结构建立

  • UUID (TEXT, Unique)
  • name (TEXT)
  • name-zh-CN (TEXT)
  • name-zh-TW (TEXT)
  • name-ja-JP (TEXT)
  • data (TEXT)
  • data-zh-CN (TEXT)
  • data-zh-TW (TEXT)
  • data-ja-JP (TEXT)

分别解释一下,UUID我设定的是武器的UUID,name是武器的名字,对应四种语言;data是皮肤数据,也是对应四种语言

然后更改了一下缓存更新的机制,改成用数据库进行存储

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import sqlite3
import requests
import json
import os
import time

sc_link = 'https://valorant-api.com/v1/weapons/skins?language=zh-CN'
tc_link = 'https://valorant-api.com/v1/weapons/skins?language=zh-TW'
jp_link = 'https://valorant-api.com/v1/weapons/skins?language=ja-JP'
en_link = 'https://valorant-api.com/v1/weapons/skins'

sc_levels_link = 'https://valorant-api.com/v1/weapons/skinlevels?language=zh-CN'
tc_levels_link = 'https://valorant-api.com/v1/weapons/skinlevels?language=zh-TW'
jp_levels_link = 'https://valorant-api.com/v1/weapons/skinlevels?language=ja-JP'
en_levels_link = 'https://valorant-api.com/v1/weapons/skinlevels'

Linkmap = [
('en', en_link),
('zh-CN', sc_link),
('zh-TW', tc_link),
('ja-JP', jp_link),
]

LinkLevelsmap = [
('en', en_levels_link),
('zh-CN', sc_levels_link),
('zh-TW', tc_levels_link),
('ja-JP', jp_levels_link),
]

def UpdateCache():
if not os.path.exists('assets/db/data.db'):
with open('assets/db/data.db', 'wb') as f:
f.close()
conn = sqlite3.connect('assets/db/data.db')
c = conn.cursor()
c.execute('CREATE TABLE skins (uuid TEXT PRIMARY KEY, name TEXT, "name-zh-CN" TEXT, "name-zh-TW" TEXT, "name-ja-JP" TEXT, data TEXT, "data-zh-CN" TEXT, "data-zh-TW" TEXT, "data-ja-JP" TEXT)')
c.execute('CREATE TABLE skinlevels (uuid TEXT PRIMARY KEY, name TEXT, "name-zh-CN" TEXT, "name-zh-TW" TEXT, "name-ja-JP" TEXT, data TEXT, "data-zh-CN" TEXT, "data-zh-TW" TEXT, "data-ja-JP" TEXT)')
conn.commit()
conn.close()



for lang, link in Linkmap:
print('Updating Skins Data of ' + lang)
conn = sqlite3.connect('assets/db/data.db')

# with open('data.json', encoding='utf8') as f:
# data = json.loads(f.read())
data = requests.get(link).json()

c = conn.cursor()
if lang == 'en':
for i in data['data']:
try:
c.execute(f'INSERT INTO skins ([uuid], name, data) VALUES (?, ?, ?)', (
i["uuid"], i["displayName"], json.dumps(i)))
conn.commit()
except sqlite3.IntegrityError:
c.execute(f'UPDATE skins SET name = ?, data = ? WHERE uuid = ?',
(i["displayName"], json.dumps(i), i["uuid"]))
conn.commit()
else:
for i in data['data']:
c.execute(f'UPDATE skins SET "name-{lang}" = ?, "data-{lang}" = ? WHERE uuid = ?',
(i["displayName"], json.dumps(i), i["uuid"]))
conn.commit()

# Delete Useless Data
# For example: Random Favorite Skin
fliter = ['Random Favorite Skin',
"Standard Classic", "Standard Shorty", "Standard Frenzy", "Standard Ghost", "Standard Sheriff",
"Standard Stinger", "Standard Spectre",
"Standard Bucky", "Standard Judge",
"Standard Bulldog", "Standard Guardian", "Standard Phantom", "Standard Vandal",
"Standard Marshal", "Standard Operator",
"Standard Ares", "Standard Odin",
"Melee"]
conn = sqlite3.connect('assets/db/data.db')
c = conn.cursor()
for ignore in fliter:
c.execute('DELETE FROM skins WHERE name = ?', (ignore,))
conn.commit()
c.close()

for lang, link in LinkLevelsmap:
print('Updating Skin Levels Data of ' + lang)
conn = sqlite3.connect('assets/db/data.db')

# with open('data.json', encoding='utf8') as f:
# data = json.loads(f.read())
data = requests.get(link).json()

c = conn.cursor()
if lang == 'en':
for i in data['data']:
try:
c.execute(f'INSERT INTO skinlevels ([uuid], name, data) VALUES (?, ?, ?)', (
i["uuid"], i["displayName"], json.dumps(i)))
conn.commit()
except sqlite3.IntegrityError:
c.execute(f'UPDATE skinlevels SET name = ?, data = ? WHERE uuid = ?',
(i["displayName"], json.dumps(i), i["uuid"]))
conn.commit()
else:
for i in data['data']:
c.execute(f'UPDATE skinlevels SET "name-{lang}" = ?, "data-{lang}" = ? WHERE uuid = ?',
(i["displayName"], json.dumps(i), i["uuid"]))
conn.commit()
c.close()

def UpdateCacheTimer():
while True:
UpdateCache()
time.sleep(3600)

if __name__ == '__main__':
UpdateCache()

在这里面,你会发现我多了一节用来清理无用数据(即上面提到的随机最爱和默认)的地方,之前我是用UUID,但是UUID实在是很难弄(你不知道对不对),所以我用武器的英文名作为判断

一开始我没有加入Standard这个单词在里面的,当时没想到会有这个,结果发现还是没有被去掉,然后看了下后台的数据才发现是有标准这个字段的

然后我才把Standard给加进去,才可以正常处理这些默认皮肤

皮肤库搜索功能

说白了就是用sqlite3的搜索功能,这也是我最后选择数据库的原因,如果要手解json的话很麻烦,而且程序运行很耗时间,所以直接用数据库语句就行了

1
SELECT uuid, name, data FROM skins WHERE name = %?%

百分号的作用就是通配符,虽然大部分的皮肤名字都是在尾巴,但是也不排除在前面(例如国服翻译的暴徒.exe,国际服台版写的是.exe 暴徒

在html中新建一个搜索框(按钮里面当然少不了瓦的标标)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<section>
<div class="container py-4">
<div class="row">
<div class="col-lg-7 mx-auto d-flex justify-content-center flex-column">
<form role="form" name="search" method="post" autocomplete="off" action="/library">
<div class="row">
<label>{{lang.library.form.search.title}}</label>
<div class="input-group">
{% if not query %}
<input type="text" class="form-control" placeholder="{{lang.library.form.search.placeholder}}"
name="query">
{% else %}
<input type="text" class="form-control" placeholder="{{lang.library.form.search.placeholder}}" value={{query}}
name="query">
{% endif %}
</div>
</div>
<br>
<div class="row">
<div class="col-md-12">
<button type="submit" class="btn bg-gradient-dark w-100"><img
src="/assets/img/img-navi-valorant-white.svg">{{lang.library.button.search}}</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>

也是用表单的方式,把搜索的内容提交到后端进行处理,后端进行字符串的拼接

1
2
3
4
query = '%' + request.form.get('query') + '%'	# 加入通配符
c.execute(f'SELECT uuid, "name-{dictlang}", "data-zh-TW" FROM skins WHERE "name-zh-CN" LIKE ? OR "name-zh-TW" LIKE ?', (query, query)) # 简中繁中一起搜索
conn.commit() # 提交语句运行
skins = c.fetchall() # 获取结果

因为武器内容的解析,搜索出来的结果因为语言的问题(简中繁中)所以不能用之前的Weaponlib类,我就直接把我那边的代码复制过来小改了一下,就丢进去用了

把武器进行解析,然后返回给render_template进行渲染就行了

附:数据库样本 -> VSC/data.db at dev · GamerNoTitle/VSC · GitHub

皮肤库根据武器进行搜索

这个就是加一大排按钮,然后点一下切换武器种类,不过做这个也挺麻烦的,因为近战武器它没有一个统一的称呼(例如个人近战单位 太极扇之类的),所以不能通过名称作为索引去做这个近战的分类

然后我发现了一个地方可以判断是否为近战,首先,下面是太极扇的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
{
"uuid": "2e4300f9-49b3-6bbe-af7c-94a6f56ff12e",
"displayName": "澄湖潋滟",
"themeUuid": "1468b29a-4a04-a34d-5b90-5da163f74e00",
"contentTierUuid": "e046854e-406c-37f4-6607-19a9ba8426fc",
"displayIcon": "https://media.valorant-api.com/weaponskins/2e4300f9-49b3-6bbe-af7c-94a6f56ff12e/displayicon.png",
"wallpaper": null,
"assetPath": "ShooterGame/Content/Equippables/Melee/Koi/Melee_Koi_PrimaryAsset",
"chromas": [
{
"uuid": "2733ffb9-4285-f7cf-e01e-dbb9314f3a96",
"displayName": "澄湖潋滟",
"displayIcon": null,
"fullRender": "https://media.valorant-api.com/weaponskinchromas/2733ffb9-4285-f7cf-e01e-dbb9314f3a96/fullrender.png",
"swatch": "https://media.valorant-api.com/weaponskinchromas/2733ffb9-4285-f7cf-e01e-dbb9314f3a96/swatch.png",
"streamedVideo": null,
"assetPath": "ShooterGame/Content/Equippables/Melee/Koi/Chromas/Standard/Melee_Koi_Standard_PrimaryAsset"
},
{
"uuid": "f6eb564b-4b08-ad4c-6704-42bf4e91453e",
"displayName": "澄湖潋滟 等级2\n(炫彩1 暗色)",
"displayIcon": null,
"fullRender": "https://media.valorant-api.com/weaponskinchromas/f6eb564b-4b08-ad4c-6704-42bf4e91453e/fullrender.png",
"swatch": "https://media.valorant-api.com/weaponskinchromas/f6eb564b-4b08-ad4c-6704-42bf4e91453e/swatch.png",
"streamedVideo": "https://valorant.dyn.riotcdn.net/x/videos/release-06.08/8a360856-412c-d772-c116-ca92aed3d809_default_universal.mp4",
"assetPath": "ShooterGame/Content/Equippables/Melee/Koi/Chromas/v1/Melee_Koi_v1_PrimaryAsset"
}
],
"levels": [
{
"uuid": "ef67d6cb-4f7f-28ce-2973-cf90a97ae54d",
"displayName": "澄湖潋滟",
"levelItem": null,
"displayIcon": "https://media.valorant-api.com/weaponskinlevels/ef67d6cb-4f7f-28ce-2973-cf90a97ae54d/displayicon.png",
"streamedVideo": "https://valorant.dyn.riotcdn.net/x/videos/release-06.08/7d2d392a-4b3f-7cea-3573-ffaf12b25589_default_universal.mp4",
"assetPath": "ShooterGame/Content/Equippables/Melee/Koi/Levels/Melee_Koi_Lv1_PrimaryAsset"
},
{
"uuid": "99a21016-41bf-2dfe-43e6-6f9ffe50d8c0",
"displayName": "澄湖潋滟 等级2",
"levelItem": "EEquippableSkinLevelItem::Animation",
"displayIcon": null,
"streamedVideo": "https://valorant.dyn.riotcdn.net/x/videos/release-06.08/229677bc-44b5-4896-131e-8097518dd336_default_universal.mp4",
"assetPath": "ShooterGame/Content/Equippables/Melee/Koi/Levels/Melee_Koi_Lv2_PrimaryAsset"
}
]
},

你会发现在assetPath里面有很明显的Melee字样,为了准确,我这里把ShooterGame/Content/Equippables/Melee/作为判断条件,如果含有这个字段则认为是近战武器,实际证明也是可行的

我们在更新缓存那一节新增一段代码,写入我们的近战武器数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
c.execute('CREATE TABLE melee (uuid TEXT PRIMARY KEY, name TEXT, "name-zh-CN" TEXT, "name-zh-TW" TEXT, "name-ja-JP" TEXT, data TEXT, "data-zh-CN" TEXT, "data-zh-TW" TEXT, "data-ja-JP" TEXT)')	# 建表
if 'ShooterGame/Content/Equippables/Melee/' in json.dumps(i):
try:
c.execute(f'INSERT INTO melee ([uuid], name, data) VALUES (?, ?, ?)', (
i["uuid"], i["displayName"], json.dumps(i)))
conn.commit()
except sqlite3.IntegrityError:
c.execute(f'UPDATE melee SET name = ?, data = ? WHERE uuid = ?',
(i["displayName"], json.dumps(i), i["uuid"]))
conn.commit()
except sqlite3.OperationalError:
c.execute(
'CREATE TABLE melee (uuid TEXT PRIMARY KEY, name TEXT, "name-zh-CN" TEXT, "name-zh-TW" TEXT, "name-ja-JP" TEXT, data TEXT, "data-zh-CN" TEXT, "data-zh-TW" TEXT, "data-ja-JP" TEXT)')
c.execute(f'INSERT INTO melee ([uuid], name, data) VALUES (?, ?, ?)', (
i["uuid"], i["displayName"], json.dumps(i)))
conn.commit()

这样,我们把所有的数据存入melee表中,然后直接获取melee表中所有的数据就是我们的近战武器了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
if request.args.get('query') not in ['近战武器', '近戰武器', 'Melee', '近接武器']:
# 非近战武器,正常查询
if lang == 'en':
# Get all skins' uuid & name
c.execute(
'SELECT uuid, name, data FROM skins WHERE name LIKE ?', (query,))
elif lang == 'zh-CN' or lang == 'zh-TW':
c.execute(
f'SELECT uuid, "name-{dictlang}", "data-zh-TW" FROM skins WHERE "name-zh-CN" LIKE ? OR "name-zh-TW" LIKE ?', (query, query))
else:
c.execute(
f'SELECT uuid, "name-{dictlang}", "data-{dictlang}" FROM skins WHERE "name-{lang}" like ?', (query,))
conn.commit()
else:
# 近战武器,直接获取melee表
if lang == 'en':
# Get all skins' uuid & name
c.execute(
'SELECT uuid, name, data FROM melee')
elif lang == 'zh-CN' or lang == 'zh-TW':
c.execute(
f'SELECT uuid, "name-{dictlang}", "data-zh-TW" FROM melee')
else:
c.execute(
f'SELECT uuid, "name-{dictlang}", "data-{dictlang}" FROM melee')
conn.commit()
skins = c.fetchall()

第四次踩坑:翻译问题

做完了分类以后,我逐个去尝试,发现一个问题

没错,捍卫者这一分类下出现了一把R8,然后我去找是什么情况,结果一查才知道这把R8的翻译叫做戍卫者

那没办法了,捍卫者这把枪的索引就只能换成繁中了(因为不冲突),想看看是啥情况的可以自己把query后面的参数改成戍卫去试试

翻译对照表

做这个功能的原因是我现在游戏内用的英语,然后如果刚进游戏没看到是什么地图,而是在选英雄的时候看地图的时候,就会不知道是什么地图,所以加了这个功能

因为加了地图的对照表,所以干脆就把所有的东西都加进去,用不同的端点区分

现在更新缓存那里把其他的内容也加入数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
LinkAgentsmap = [
('en', 'https://valorant-api.com/v1/agents'),
('zh-CN', 'https://valorant-api.com/v1/agents?language=zh-CN'),
('zh-TW', 'https://valorant-api.com/v1/agents?language=zh-TW'),
('ja-JP', 'https://valorant-api.com/v1/agents?language=ja-JP'),
]

LinkMapsmap = [
('en', 'https://valorant-api.com/v1/maps'),
('zh-CN', 'https://valorant-api.com/v1/maps?language=zh-CN'),
('zh-TW', 'https://valorant-api.com/v1/maps?language=zh-TW'),
('ja-JP', 'https://valorant-api.com/v1/maps?language=ja-JP'),
]

LinkWeaponsmap = [
('en', 'https://valorant-api.com/v1/weapons'),
('zh-CN', 'https://valorant-api.com/v1/weapons?language=zh-CN'),
('zh-TW', 'https://valorant-api.com/v1/weapons?language=zh-TW'),
('ja-JP', 'https://valorant-api.com/v1/weapons?language=ja-JP'),
]

c.execute(
'CREATE TABLE agents (uuid TEXT PRIMARY KEY, name TEXT, "name-zh-CN" TEXT, "name-zh-TW" TEXT, "name-ja-JP" TEXT)')
c.execute(
'CREATE TABLE weapons (uuid TEXT PRIMARY KEY, name TEXT, "name-zh-CN" TEXT, "name-zh-TW" TEXT, "name-ja-JP" TEXT)')
c.execute(
'CREATE TABLE maps (uuid TEXT PRIMARY KEY, name TEXT, "name-zh-CN" TEXT, "name-zh-TW" TEXT, "name-ja-JP" TEXT)')

for lang, link in LinkAgentsmap:
print('Updating Agents Data of ' + lang)
conn = sqlite3.connect('db/data.db')
data = requests.get(link).json()

c = conn.cursor()
if lang == 'en':
for i in data['data']:
if i['isPlayableCharacter']: # There's an unplayable SOVA in data
try:
c.execute(f'INSERT INTO agents ([uuid], name) VALUES (?, ?)', (
i["uuid"], i["displayName"]))
conn.commit()
except sqlite3.IntegrityError:
c.execute(f'UPDATE agents SET name = ? WHERE uuid = ?',
(i["displayName"], i["uuid"]))
conn.commit()
else:
if i['isPlayableCharacter']:
for i in data['data']:
c.execute(f'UPDATE agents SET "name-{lang}" = ? WHERE uuid = ?',
(i["displayName"], i["uuid"]))
conn.commit()
c.close()

for lang, link in LinkMapsmap:
print('Updating Maps Data of ' + lang)
conn = sqlite3.connect('db/data.db')
data = requests.get(link).json()

c = conn.cursor()
if lang == 'en':
for i in data['data']:
try:
c.execute(f'INSERT INTO maps ([uuid], name) VALUES (?, ?)', (
i["uuid"], i["displayName"]))
conn.commit()
except sqlite3.IntegrityError:
c.execute(f'UPDATE maps SET name = ? WHERE uuid = ?',
(i["displayName"], i["uuid"]))
conn.commit()
else:
for i in data['data']:
c.execute(f'UPDATE maps SET "name-{lang}" = ? WHERE uuid = ?',
(i["displayName"], i["uuid"]))
conn.commit()
c.close()

for lang, link in LinkWeaponsmap:
print('Updating Weapons Data of ' + lang)
conn = sqlite3.connect('db/data.db')
data = requests.get(link).json()

c = conn.cursor()
if lang == 'en':
for i in data['data']:
try:
c.execute(f'INSERT INTO weapons ([uuid], name) VALUES (?, ?)', (
i["uuid"], i["displayName"]))
conn.commit()
except sqlite3.IntegrityError:
c.execute(f'UPDATE weapons SET name = ? WHERE uuid = ?',
(i["displayName"], i["uuid"]))
conn.commit()
else:
for i in data['data']:
c.execute(f'UPDATE weapons SET "name-{lang}" = ? WHERE uuid = ?',
(i["displayName"], i["uuid"]))
conn.commit()
c.close()

说白了就是照葫芦画瓢,因为之前做过了

在主程序里面加入数据库查询的功能就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@app.route('/trans')
def transDefault():
return redirect('/trans/maps')


@app.route('/trans/<t>')
def trans(t):
if request.args.get('lang'):
if request.args.get('lang') in app.config['BABEL_LANGUAGES']:
lang = request.args.get('lang')
else:
lang = str(request.accept_languages.best_match(
app.config['BABEL_LANGUAGES']))
elif request.accept_languages.best_match(app.config['BABEL_LANGUAGES']):
lang = str(request.accept_languages.best_match(
app.config['BABEL_LANGUAGES']))
else:
lang = 'en'
if t in ['agents', 'maps', 'weapons', 'skins']:
conn = sqlite3.connect('db/data.db')
datalist = []
if t == 'skins':
c = conn.cursor()
c.execute(
'SELECT name, "name-zh-CN", "name-zh-TW", "name-ja-JP", isMelee FROM {}'.format(t))
conn.commit()
data = c.fetchall()
c.execute(
'SELECT name, "name-zh-CN", "name-zh-TW", "name-ja-JP" FROM weapons')
conn.commit()
weapons = c.fetchall()
else:
c = conn.cursor()
c.execute(
'SELECT name, "name-zh-CN", "name-zh-TW", "name-ja-JP" FROM {}'.format(t))
conn.commit()
data = c.fetchall()
for i in data:
if t == 'skins':
en_name, zhCN_name, zhTW_name, jaJP_name, isMelee = i
if isMelee:
continue
for en, zhCN, zhTW, jaJP in weapons:
en_name = en_name.replace(en, '').strip()
zhCN_name = zhCN_name.replace(zhCN, '').strip()
zhTW_name = zhTW_name.replace(zhTW, '').strip()
jaJP_name = jaJP_name.replace(jaJP, '').strip()
if {"en": en_name, "zhCN": zhCN_name, "zhTW": zhTW_name, "jaJP": jaJP_name} not in datalist:
datalist.append(
{"en": en_name, "zhCN": zhCN_name, "zhTW": zhTW_name, "jaJP": jaJP_name})
else:
if {"en": i[0], "zhCN": i[1], "zhTW": i[2], "jaJP": i[3]} not in datalist:
datalist.append({"en": i[0], "zhCN": i[1],
"zhTW": i[2], "jaJP": i[3]})
return render_template('trans.html', data=list(datalist), lang=yaml.load(os.popen(
f'cat lang/{lang}.yml').read(), Loader=yaml.FullLoader))
else:
abort(404)

手动切换网站语言

这个还是@Vanilluv提出的,我就加了个lang参数用来切换语言

如果是网站支持的语言中的一种,就显示指定的语言;如果非支持的语言,默认显示浏览器语言;如果浏览器没有合适的语言(例如用curl访问)就会显示英语

按照这个逻辑,写了一个判断

1
2
3
4
5
6
7
8
9
10
11
12
13
if request.args.get('lang'):
if request.args.get('lang') in app.config['BABEL_LANGUAGES']:
lang = request.args.get('lang')
elif request.accept_languages.best_match(app.config['BABEL_LANGUAGES']):
lang = str(request.accept_languages.best_match(
app.config['BABEL_LANGUAGES']))
else:
lang = 'en'
elif request.accept_languages.best_match(app.config['BABEL_LANGUAGES']):
lang = str(request.accept_languages.best_match(
app.config['BABEL_LANGUAGES']))
else:
lang = 'en'

然后使用语言文件的时候就直接调用{lang}.yml就可以了

库存查看功能

这个功能是有人在小黑盒里面问到说“是否支持查看库存”(@Vanilluv发每日商店的时候有人问的),然后本来就想做这个功能,所以就花了三天的时间做了

获取库存很简单,在参考文档里面就有这节内容

获取玩家已拥有的物品 /store/v1/entitlements/{userId}/{itemType}

所以获取是很好弄的,获取到的内容我也看了一下,是所有已经购买的武器的等级(需要找skinlevels表)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"ItemTypeID": "e7c63390-eda7-46e0-bb7a-a6abdacd2433",
"Entitlements": [
{
"TypeID": "4e60e748-bce6-4faa-9327-ebbe6089d5fe",
"ItemID": "12831559-44ee-c708-b97c-29a43938e3cd"
},
... (此处省略)
{
"TypeID": "4e60e748-bce6-4faa-9327-ebbe6089d5fe",
"ItemID": "eaf52d49-4608-45d9-5f18-c8b12614e01f"
}
]
}

ItemID就是skinlevels表里面的uuid,所以直接跟着UUID找就行了

而炫彩就没那么简单了,炫彩虽然也是uuid,但是我们没有构造炫彩的数据库表(下面是个例子)

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"ItemTypeID": "3ad1b2b2-acdb-4524-852f-954a76ddae0a",
"Entitlements": [
{
"TypeID": "4e60e748-bce6-4faa-9327-ebbe6089d5fe",
"ItemID": "2d0c7deb-4f5a-40ea-8fe8-288e060d02c6"
},
{
"TypeID": "4e60e748-bce6-4faa-9327-ebbe6089d5fe",
"ItemID": "7a68a469-4c7b-1c17-372d-2dbcd50fc114"
}
]
}

然后我是先把有的做了出来,就只要是获取到皮肤的UUID,发现是等级1的,就判断有这把枪,然后把这把枪的数据加入weapon_list中给网页进行渲染,就达到了查看库存的效果

显示已购买的等级

为什么我上面说花了三天,因为在想怎么给用户提示说这个等级已经用RP购买过了,上面我们不是说到过获取库存的时候是获取的所有皮肤等级嘛,我就把玩家拥有的皮肤等级的UUID给弄到一个列表,然后获取皮肤信息的时候,如果这个等级的UUID在我的列表里面,就给个FLAG叫udpated,设定为True,渲染的时候就只需要检查是否存在updated这个FLAG就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class player:
def getSkins(self):
data = requests.get(f'{self.server}{Api.owned}{self.user_id}/{Options.skins}', headers=self.__header, timeout=30).json()
skins = data['Entitlements']
owned_skins = []
for skin in skins:
owned_skins.append(skin['ItemID'].upper())
return skins, owned_skins # 这里owned_skins就是已拥有的皮肤列表

def getChromas(self):
data = requests.get(f'{self.server}{Api.owned}{self.user_id}/{Options.chromas}', headers=self.__header, timeout=30).json()
chromas = data['Entitlements']
owned_chromas = []
for chroma in chromas:
owned_chromas.append(chroma['ItemID'].upper())
return chromas, owned_chromas # 这里owned_chromas就是已拥有的炫彩列表

1
2
3
4
5
6
7
8
9
10
11
12
{% if level.updated %} <!-- If this level has been updated, set the color of the button to green -->
<button type="button" class="btn bg-gradient-success" data-bs-toggle="modal"
data-bs-target="#WEAPON-{{level.uuid|escape}}">
{{level.displayName}}: {{level.levelItem}}
</button>
{% else %}
<button type="button" class="btn bg-gradient-primary" data-bs-toggle="modal"
data-bs-target="#WEAPON-{{level.uuid|escape}}">
{{level.displayName}}: {{level.levelItem}}
</button>
{% endif %}

这样就可以达到变绿的效果,而原来的紫色按钮就说明没有购买这个等级/炫彩

在皮肤库/库存中显示价格

首先这个价格的来源就很麻烦。我尝试使用拳头自己的Offer列表去解析价格,但是怎么弄价格都是错的(鬼刀4800那种错误),所以没办法。

后来我找到了VALORANT FANDOM WIKI,里面有各种武器的价格

然后就开始弄解析它那个价格表格的代码了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
def UpdatePriceCache():
print('Start Updating Price Cache')
weapons = [
"Classic", "Shorty", "Frenzy", "Ghost", "Sheriff",
"Stinger", "Spectre",
"Bucky", "Judge",
"Bulldog", "Guardian", "Phantom", "Vandal",
"Marshal", "Operator",
"Ares", "Odin",
"Tactical_Knife"]
base_url = 'https://valorant.fandom.com/wiki' # Get price data from wiki
for weapon in weapons:
print(f'Updating {weapon.replace("_", " ")}')
res = requests.get(f'{base_url}/{weapon}')
html = res.text
soup = BeautifulSoup(html, 'html.parser')
tables = soup.find_all('table', class_='wikitable sortable')
if len(tables) > 0:
table = tables[0]
for row in table.find_all('tr'):
cells = row.find_all('td')
if len(cells) == 0:
continue
content = [cell.text for cell in cells]
# Content Format: [Image, Edition, Collection, Source, Cost/Unlock, (Upgrades), (Chromas)]
if weapon != 'Tactical_Knife':
collection = content[2].replace('\n', '')
source = content[3].replace('\n', '')
unlock = content[4].replace('\n', '')
weapon_name = f'{collection} {weapon}'
else:
collection = content[1].replace('\n', '')
name = content[2].replace('\n', '')
source = content[3].replace('\n', '')
unlock = content[4].replace('\n', '')
weapon_name = f'{collection} {name}'
conn = sqlite3.connect('db/data.db')
c = conn.cursor()
try:
vp_img = '<img src="/assets/img/VP-black.png" width="32px" height="32px">'
if source == 'Store': # This skin can be unlocked through store
c.execute('UPDATE skinlevels SET unlock = ? WHERE name LIKE ?', (f'{vp_img} {unlock}', weapon_name))
else:
c.execute('UPDATE skinlevels SET unlock = ? WHERE name LIKE ?', (f'{source} {unlock}', weapon_name))
except Exception as e:
print(e)
conn.commit()
print(f'{weapon.replace("_", " ")} has been Updated.')

因为价格基本不会改,而且只有拳头出皮肤的时候才会有价格的变动,所以我设置为12小时更新一次了

然后把价格/解锁方式给丢入数据库,渲染HTML的时候把数据取出渲染就是了

FAQ的编写

这个是我去游泳的那天晚上在车上想到的,因为说白了EULA那个东西,是我用生成器弄出来的,其实并不标准,而且我们这个是开源项目,也不是公司什么的那种主体提供的服务,原则上我只需要写个免责声明就行了,但是写个免责声明吧,有感觉好像缺了点什么,最后就决定写个FAQ

所以FAQ在这里啦:https://gamernotitle.notion.site/VALORA-FAQ-86f072f8cebf4a8d9453a795b24cd507

改名风波

嗯没错,VSC改名为了Valora,这是因为@Vanilluv给我发了张图

也不知道是不是这个人开了还是咋的,反正就是被封了,但是本着开发者的态度,他说用了VSC我就去查查是不是我这个,我就去翻数据库(数据库有玩家的ID,小黑盒是可以查玩家的ID的),然后没发现,就说明咱们是清白的

VSC这个名字确实很多东西用,我之前查到过(如图,这还是其中一个)

所以说咱们为了不跟人冲突就改个名字嘛,我就去问ChatGPT了,改什么名字好

  • 我开发了一个查看瓦罗兰特每日商店的网站,请你帮我给这个网站想个名字,要英文的,越短越好,而且要有缩写版

    • How about “ValShop” as the name for your website? It’s short, easy to remember, and has a clear connection to the subject matter. For the abbreviation, you could use “VS” which is simple and recognizable.
  • 换一个

    • Sure, here are a few more suggestions for the name of your website:

      1. “DailyVault”: This name combines “daily” to indicate that the site is updated every day, and “vault” to reference the setting of the game.
      2. “ValoraMart”: This name plays off the popular retail chain Walmart, and incorporates “Valora” to reference the game world.
      3. “ValoraDeals”: This name emphasizes the focus on shopping and finding good deals within the Valoran setting.

      For abbreviations, you could use “DV” for “DailyVault,” “VM” for “ValoraMart,” and “VD” for “ValoraDeals.”

  • 它的功能不只有看商店,也可以查看夜市、查看库存、还有完整的皮肤资料库,请你加上这些依据再想想

    • Thank you for the additional information! Here are some more suggestions for the name of your website, taking into account the expanded functionality:

      1. “ValoraHub”: This name suggests that your website is a central hub or resource for all things related to Valoran commerce, including daily shops, night markets, inventories, and skin information.
      2. “ValoraMarket”: This name emphasizes the commerce aspect of your website, and can encompass both daily shops and night markets, as well as inventory and skin information.
      3. “ValoraVault”: This name plays off the idea of a vault or treasure trove, and suggests that your website is a valuable resource for all things Valoran, including commerce and skin information.

      For abbreviations, you could use “VH” for “ValoraHub,” “VM” for “ValoraMarket,” and “VV” for “ValoraVault.”

  • 缩写请增至三个字母 请你再想想

    • Sure, here are revised names and abbreviations that are three letters long:

      1. “ValHub” (VHB): This name is a shortened version of “ValoraHub” and still suggests that your website is a central resource for all things Valoran, including commerce and skin information.
      2. “ValMar” (VLM): This name is a shortened version of “ValoraMarket” and still emphasizes the commerce aspect of your website, while also encompassing night markets, inventory, and skin information.
      3. “ValVault” (VVT): This name is a shortened version of “ValoraVault” and still plays off the idea of a vault or treasure trove, suggesting that your website is a valuable resource for all things Valoran.

      I hope these suggestions are helpful!

嗯它给我的回答……我觉得很不错,然后最后选择了它在两个字母缩写那里给我起的前缀Valora,简称VLR

Access Token登录

本来这一节是叫做Cookie登录的,但是后来发现再怎么弄Cookie都过不了Cloudflare的WAF那一关,所以还不如直接用access token

确定用Access Token了以后就比较简单了,按照重新认证的思路做就是了,不过这样就会发现,跟账号密码登录出来的功能是有区别的:

  • 获取不到用户的基础信息(用户名和tag)
  • 无法重新获取access_token(即无法保活)

那没办法了,想要安全那得有付出,至少在我能通过WAF之前,这两个东西都不可能做好了……

按钮的点亮条件

我之前是用后端进行限制的,如果没有填写用户名/密码或者不打钩的话就直接弹出提示且不让登陆,不过这样的话说实在的,东西都发出去了,服务器再弹回来,其实就没有发送的必要,然后我就让GPT给我写了对应的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
function checkInputs() {
const accessTokenInput = document.getElementById('accesstoken-input');
const userIdInput = document.getElementById('userid-input');
const regionInput = document.getElementById('region-input');
const checkedEULAInput = document.getElementById('checked-eula-input');
const submitButton = document.getElementById('tokenlogin-submit');

// 检查accessToken是否非空
if (accessTokenInput.value.trim() === '') {
submitButton.disabled = true;
return;
}

// 检查userId是否为UUID格式
const uuidRegex = /^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$/;
if (!uuidRegex.test(userIdInput.value)) {
submitButton.disabled = true;
return;
}

// 检查region是否为有效的地区代码
const validRegions = ['ap', 'kr', 'eu', 'na'];
if (!validRegions.includes(regionInput.value.toLowerCase())) {
submitButton.disabled = true;
return;
}

// 检查checkbox是否都勾选
if (!checkedEULAInput.checked) {
submitButton.disabled = true;
return;
}

// 如果所有条件都满足,则去掉按钮的disabled属性
submitButton.disabled = false;
}
function checkLoginInputs() {
const usernameInput = document.getElementsByName('Username')[0];
const passwordInput = document.getElementsByName('Password')[0];
const checkedRuleInput = document.getElementsByName('CheckedRule')[0];
const checkedEULAInput = document.getElementsByName('CheckedEULA')[0];
const submitButton = document.getElementById('login');

// 检查username和password是否非空
if (usernameInput.value.trim() === '' || passwordInput.value.trim() === '') {
submitButton.disabled = true;
return;
}

// 检查checkbox是否都勾选
if (!checkedRuleInput.checked || !checkedEULAInput.checked) {
submitButton.disabled = true;
return;
}

// 如果所有条件都满足,则去掉按钮的disabled属性
submitButton.disabled = false;
}

然后把元素的ID一改,完事!

结语

这个项目真的是从我自己立项开始做到现在,做了两周有多,接下来还有其他的更新,但是也是慢更了,就是那种小小的更新,功能性的除了一个皮肤库还没写以外,我就想不到还能做什么功能了,如果你有好的建议可以在下面评论,我看到会去试试的

如果你想给我赞助,除了访问赞助页面以外,也可以给我的账号充VP(马来区:PINK LEMONADE#Lumi),PM(私聊)我我看到会给你发ID的,谢谢!