Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit2d1ad37

Browse files
committed
Rather large update for client, frontend, backend
I'm planning on stepping back from the idea of creating accounts to use the website. It will still include the player's activity log and various other things, but that will probably be it.
1 parent1c3066a commit2d1ad37

25 files changed

+433
-968
lines changed

‎api/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Created by Deltaion Lee (MCMi460) on Github
22

33
from .loveimport*# fc2pi
4+
from .utilimport*# utility
45
# --
56
# Following is unnecessary for production
67
# from .main import * # main loop

‎api/util.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Created by Deltaion Lee (MCMi460) on Github
2+
3+
importos,sys
4+
importsqlite3
5+
6+
defgetAppPath():# Credit to @HotaruBlaze
7+
applicationPath=os.path.expanduser('~/Documents/3DS-RPC')
8+
# Windows allows you to move your UserProfile subfolders, Such as Documents, Videos, Music etc.
9+
# However os.path.expanduser does not actually check and assumes it's in the default location.
10+
# This tries to correctly resolve the Documents path and fallbacks to default if it fails.
11+
ifos.name=='nt':
12+
try:
13+
importctypes.wintypes
14+
CSIDL_PERSONAL=5# My Documents
15+
SHGFP_TYPE_CURRENT=0# Get current, not default value
16+
buf=ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH)
17+
ctypes.windll.shell32.SHGetFolderPathW(None,CSIDL_PERSONAL,None,SHGFP_TYPE_CURRENT,buf)
18+
applicationPath=os.path.join(buf.value,'3DS-RPC')
19+
except:pass
20+
returnapplicationPath
21+
22+
defstartDBTime(time):
23+
withsqlite3.connect('sqlite/fcLibrary.db')ascon:
24+
cursor=con.cursor()
25+
cursor.execute('DELETE FROM config')
26+
cursor.execute('INSERT INTO config (BACKEND_UPTIME) VALUES (%s)'% (time,))
27+
con.commit()
28+
29+
classProgressBar():# Written with help from https://stackoverflow.com/a/3160819/11042767
30+
def__init__(self,width:int):
31+
self.width=width
32+
sys.stdout.write('[%s]'% (' '*width))
33+
sys.stdout.flush()
34+
sys.stdout.write('\b'* (width+1))
35+
36+
defupdate(self,fraction:float):
37+
forninrange(int(fraction*self.width)):
38+
sys.stdout.write('-')
39+
sys.stdout.flush()
40+
41+
defend(self):
42+
sys.stdout.write(']\n')

‎client/client.py

Lines changed: 67 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22

33
importrequests,os,sys,time
44
importxmltodict,json
5+
importpickle
56
sys.path.append('../')
67
fromapiimport*
78
importpypresence
89
fromtypingimportLiteral,get_args
910

11+
requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)
12+
1013
local=False
11-
version=0.2
14+
version=0.21
1215

1316
host='https://3ds.mi460.dev'# Change the host as you'd wish
1417
iflocal:
@@ -19,24 +22,9 @@
1922
## on running your own front and backend.
2023
convertFriendCodeToPrincipalId(botFC)# A quick verification check
2124

22-
defgetAppPath():# Credit to @HotaruBlaze
23-
applicationPath=os.path.expanduser('~/Documents/3DS-RPC')
24-
# Windows allows you to move your UserProfile subfolders, Such as Documents, Videos, Music etc.
25-
# However os.path.expanduser does not actually check and assumes it's in the default location.
26-
# This tries to correctly resolve the Documents path and fallbacks to default if it fails.
27-
ifos.name=='nt':
28-
try:
29-
importctypes.wintypes
30-
CSIDL_PERSONAL=5# My Documents
31-
SHGFP_TYPE_CURRENT=0# Get current, not default value
32-
buf=ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH)
33-
ctypes.windll.shell32.SHGetFolderPathW(None,CSIDL_PERSONAL,None,SHGFP_TYPE_CURRENT,buf)
34-
applicationPath=os.path.join(buf.value,'3DS-RPC')
35-
except:pass
36-
returnapplicationPath
37-
38-
_REGION=Literal['US','JP','GB','KR','TW']
25+
_REGION=Literal['US','JP','GB','KR','TW','ALL']
3926
path=getAppPath()
27+
privateFile=os.path.join(path,'private.txt')
4028

4129
classAPIException(Exception):
4230
pass
@@ -48,11 +36,11 @@ class GameMatchError(Exception):
4836
pass
4937

5038
classClient():
51-
def__init__(self,region:_REGION,friendCode:str):
39+
def__init__(self,region:_REGION,friendCode:str,saveTitleFiles:bool=True):
5240
### Maintain typing ###
5341
assertregioninget_args(_REGION),'\'%s\' does not match _REGION'%region# Region assertion
54-
convertFriendCodeToPrincipalId(friendCode)# Friend Code check
55-
withopen(os.path.join(path,'private.txt'),'w')asfile:# Save FC to file
42+
friendCode=str(convertPrincipalIdtoFriendCode(convertFriendCodeToPrincipalId(friendCode))).zfill(12)# Friend Code check
43+
withopen(privateFile,'w')asfile:# Save FC to file
5644
file.write(json.dumps({
5745
'friendCode':friendCode,
5846
'region':region,
@@ -68,14 +56,44 @@ def __init__(self, region: _REGION, friendCode: str):
6856
self.currentGame= {'@id':None}
6957

7058
# Pull databases
71-
self.titleDatabase=xmltodict.parse(requests.get('https://samurai.ctr.shop.nintendo.net/samurai/ws/%s/titles?shop_id=1&limit=5000&offset=0'%self.region,verify=False).text)
72-
self.titlesToUID=requests.get('https://raw.githubusercontent.com/hax0kartik/3dsdb/master/jsons/list_%s.json'%self.region).json()
73-
## Warning; the above does not account for games being played by a user who has removed the region lock on their system
74-
## Please consider fixing this in the future, @MCMi460
59+
self.region= (self.region,)
60+
ifself.region[0]=='ALL':
61+
self.region=list(get_args(_REGION))
62+
delself.region[-1]
63+
databasePath=os.path.join(path,'databases.dat')
64+
ifos.path.isfile(databasePath):
65+
withopen(databasePath,'rb')asfile:
66+
t=pickle.loads(file.read())
67+
self.titleDatabase=t[0]
68+
self.titlesToUID=t[1]
69+
else:
70+
self.titleDatabase= []
71+
self.titlesToUID= []
72+
73+
bar=ProgressBar(40)# Create progress bar
74+
75+
forregioninself.region:
76+
self.titleDatabase.append(
77+
xmltodict.parse(requests.get('https://samurai.ctr.shop.nintendo.net/samurai/ws/%s/titles?shop_id=1&limit=5000&offset=0'%region,verify=False).text)
78+
)
79+
bar.update(.5/len(self.region))# Update progress bar
80+
self.titlesToUID+=requests.get('https://raw.githubusercontent.com/hax0kartik/3dsdb/master/jsons/list_%s.json'%region,stream=True).json()
81+
bar.update(.5/len(self.region))# Update progress bar
82+
83+
bar.end()# End the progress bar
84+
85+
# Save databases to file
86+
ifsaveTitleFilesandnotos.path.isfile(databasePath):
87+
withopen(databasePath,'wb')asfile:
88+
file.write(pickle.dumps(
89+
(self.titleDatabase,
90+
self.titlesToUID)
91+
))
92+
print('[Saved database to file]')
7593

7694
# Get from API
7795
defAPIget(self,route:str,content:dict= {}):
78-
returnrequests.get(host+'/'+route,data=content,headers= {'User-Agent':'3DS-RPC/%s'%version,})
96+
returnrequests.get(host+'/api/'+route,data=content,headers= {'User-Agent':'3DS-RPC/%s'%version,})
7997

8098
# Connect to PyPresence
8199
defconnect(self):
@@ -97,10 +115,6 @@ def fetch(self):
97115
defloop(self):
98116
userData=self.fetch()
99117
presence=userData['User']['Presence']
100-
ifuserData['User']['notifications']:
101-
notifications= [json.loads(n)forninuserData['User']['notifications'].split('|') ]
102-
else:
103-
notifications= []
104118

105119
_pass=None
106120
ifuserData['User']['online']andpresence:
@@ -124,10 +138,11 @@ def loop(self):
124138
# raise TitleIDMatchError('unknown title id: %s' % tid)
125139

126140
game=None
127-
fortitleinself.titleDatabase['eshop']['contents']['content']:
128-
iftitle['title']['@id']==uid:
129-
game=title['title']
130-
break
141+
forregioninself.titleDatabase:
142+
fortitleinregion['eshop']['contents']['content']:
143+
iftitle['title']['@id']==uid:
144+
game=title['title']
145+
break
131146
ifnotgame:
132147
_pass=_template
133148
# raise GameMatchError('unknown game: %s' % uid)
@@ -145,9 +160,9 @@ def loop(self):
145160
large_image=game['icon_url'].replace('https://kanzashi-ctr.cdn.nintendo.net/i/',host+'/cdn/i/'),
146161
large_text=game['name'],
147162
start=self.start,
148-
# buttons = [{'label': 'Add Friend', 'url':host + '/f/%s' % convertPrincipalIdtoFriendCode(convertFriendCodeToPrincipalId(self.friendCode))},]
163+
# buttons = [{'label': 'Label', 'url':'http://DOMAIN.WHATEVER'},]
149164
# eShop URL could be https://api.qrserver.com/v1/create-qr-code/?data=ESHOP://{uid}
150-
# But that's dumb so no
165+
# But... that wouldn't be very convenient. It's unfortunate how Nintendo does not have an eShop website for the 3DS
151166
)
152167
else:
153168
print('Clear [%s -> %s]'% (self.currentGame['@id'],None))
@@ -161,21 +176,31 @@ def main():
161176
# Create directory for logging and friend code saving
162177
ifnotos.path.isdir(path):
163178
os.mkdir(path)
164-
privateFile=os.path.join(path,'private.txt')
165179
ifnotos.path.isfile(privateFile):
166180
print('Please take this time to add the bot\'s FC to your target 3DS\' friends list.\nBot FC: %s'%'-'.join(botFC[i:i+4]foriinrange(0,len(botFC),4)))
167181
input('[Press enter to continue]')
168-
friendCode=input('Please enter your 3DS\' friend code\n> ')
169-
friendCode=str(convertPrincipalIdtoFriendCode(convertFriendCodeToPrincipalId(friendCode))).zfill(12)# Check validity to prevent writing invalid FC
182+
friendCode=input('Please enter your 3DS\' friend code\n>\033[0;35m')
183+
print('\033[0m',end='')
170184
else:
171185
withopen(privateFile,'r')asfile:
172186
js=json.loads(file.read())
173187
friendCode=js['friendCode']
174188
region=js.get('region')
175189
ifnotregion:
176-
region=input('Please enter your 3DS\' region [%s]\n> '%', '.join(get_args(_REGION)))
177-
178-
client=Client(region,friendCode)
190+
region=input('Please enter your 3DS\' region [%s]\n\033[93m(You may enter\'ALL\' if you are planning to play with multiple regions\' games)\033[0m\n>\033[0;35m'%', '.join(get_args(_REGION)))
191+
print('\033[0m',end='')
192+
ifregion=='ALL':
193+
r=input('-\033[91mEnabling ALL regions may take a few minutes to download. Is this agreeable?\033[0m\n- >\033[0;35m')
194+
ifnotr.lower().startswith('y'):
195+
return
196+
print('\033[0m')
197+
198+
try:
199+
client=Client(region,friendCode)
200+
except (AssertionError,FriendCodeValidityError)ase:
201+
ifos.path.isfile(privateFile):
202+
os.remove(privateFile)
203+
raisee
179204
whileTrue:
180205
client.loop()
181206
time.sleep(30)

‎server/backend.py

Lines changed: 60 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
delay=2
1616
since=0
17+
startDBTime(time.time())
1718

1819
asyncdefmain():
1920
whileTrue:
@@ -41,70 +42,73 @@ async def main():
4142
time.sleep(delay)
4243

4344
print('Grabbing new friends...')
44-
con=sqlite3.connect('sqlite/fcLibrary.db')
45-
cursor=con.cursor()
46-
47-
cursor.execute('SELECT friendCode, lastAccessed FROM friends')
48-
result=cursor.fetchall()
49-
ifnotresult:
50-
continue
51-
list= []
52-
forrowinresult:
53-
iftime.time()-row[1]<=30:
54-
list.append(row[0])
55-
56-
lst= [convertFriendCodeToPrincipalId(f)forfinlist ]
57-
# Obviously change the above lines, but the premise remains
58-
#lst = [ convertFriendCodeToPrincipalId(privFriend), ] # Debug purposes currently
59-
#print(lst)
60-
61-
foriinrange(0,len(lst),100):
62-
rotation=lst[i:i+100]
63-
64-
removeList= []
65-
time.sleep(delay)
66-
awaitfriends_client.add_friend_by_principal_ids(0,rotation)
67-
68-
time.sleep(delay)
69-
t=awaitfriends_client.get_all_friends()
70-
iflen(t)<len(rotation):
71-
forIDinrotation:
72-
ifIDnotin [f.unk1forfint ]:
73-
removeList.append(ID)
74-
75-
forremoverinremoveList:
76-
cursor.execute('DELETE FROM friends WHERE friendCode =\'%s\''%str(convertPrincipalIdtoFriendCode(remover)).zfill(12))
77-
cursor.execute('DELETE FROM auth WHERE friendCode =\'%s\''%str(convertPrincipalIdtoFriendCode(remover)).zfill(12))
78-
con.commit()
79-
80-
iflen(t)>0:
45+
withsqlite3.connect('sqlite/fcLibrary.db')ascon:
46+
cursor=con.cursor()
47+
48+
cursor.execute('SELECT friendCode, lastAccessed FROM friends')
49+
result=cursor.fetchall()
50+
ifnotresult:
51+
continue
52+
list= []
53+
forrowinresult:
54+
iftime.time()-row[1]<=30:
55+
list.append(row[0])
56+
57+
lst= [convertFriendCodeToPrincipalId(f)forfinlist ]
58+
# Obviously change the above lines, but the premise remains
59+
#lst = [ convertFriendCodeToPrincipalId(privFriend), ] # Debug purposes currently
60+
#print(lst)
61+
62+
foriinrange(0,len(lst),100):
63+
rotation=lst[i:i+100]
64+
65+
removeList= []
8166
time.sleep(delay)
82-
f=awaitfriends_client.get_friend_presence([e.unk1foreint ])
83-
users= []
84-
forgameinf:
85-
# game.unk == principalId
86-
users.append(game.unk)
87-
#print(game.presence.game_mode_description)
88-
cursor.execute('UPDATE friends SET online = %s, titleID = %s, updID = %s WHERE friendCode =\'%s\''% (True,game.presence.game_key.title_id,game.presence.game_key.title_version,str(convertPrincipalIdtoFriendCode(users[-1])).zfill(12)))
89-
foruserin [hforhinrotationifnothinusers ]:
90-
cursor.execute('UPDATE friends SET online = %s, titleID = %s, updID = %s WHERE friendCode =\'%s\''% (False,0,0,str(convertPrincipalIdtoFriendCode(user)).zfill(12)))
91-
92-
fortiint:
93-
time.sleep(delay)
94-
m=awaitfriends_client.get_friend_mii([ti,])# I can't figure out get_friend_mii_list :(
95-
j1=awaitfriends_client.get_friend_comment([ti,])
96-
cursor.execute('UPDATE friends SET username =\'%s\', message =\'%s\' WHERE friendCode =\'%s\''% (m[0].mii.unk1,j1[0].comment,str(convertPrincipalIdtoFriendCode(m[0].unk1)).zfill(12)))
67+
awaitfriends_client.add_friend_by_principal_ids(0,rotation)
9768

69+
time.sleep(delay)
70+
t=awaitfriends_client.get_all_friends()
71+
iflen(t)<len(rotation):
72+
forIDinrotation:
73+
ifIDnotin [f.unk1forfint ]:
74+
removeList.append(ID)
75+
76+
forremoverinremoveList:
77+
cursor.execute('DELETE FROM friends WHERE friendCode =\'%s\''%str(convertPrincipalIdtoFriendCode(remover)).zfill(12))
78+
cursor.execute('DELETE FROM auth WHERE friendCode =\'%s\''%str(convertPrincipalIdtoFriendCode(remover)).zfill(12))
9879
con.commit()
9980

100-
time.sleep(delay)
101-
forfriendinrotation:
102-
awaitfriends_client.remove_friend_by_principal_id(friend)
81+
iflen(t)>0:
82+
time.sleep(delay)
83+
f=awaitfriends_client.get_friend_presence([e.unk1foreint ])
84+
users= []
85+
forgameinf:
86+
# game.unk == principalId
87+
users.append(game.unk)
88+
#print(game.presence.game_mode_description)
89+
cursor.execute('UPDATE friends SET online = %s, titleID = %s, updID = %s WHERE friendCode =\'%s\''% (True,game.presence.game_key.title_id,game.presence.game_key.title_version,str(convertPrincipalIdtoFriendCode(users[-1])).zfill(12)))
90+
foruserin [hforhinrotationifnothinusers ]:
91+
cursor.execute('UPDATE friends SET online = %s, titleID = %s, updID = %s WHERE friendCode =\'%s\''% (False,0,0,str(convertPrincipalIdtoFriendCode(user)).zfill(12)))
92+
93+
fortiint:
94+
time.sleep(delay)
95+
m=awaitfriends_client.get_friend_mii([ti,])# I can't figure out get_friend_mii_list :(
96+
j1=awaitfriends_client.get_friend_comment([ti,])
97+
cursor.execute('UPDATE friends SET username =\'%s\', message =\'%s\' WHERE friendCode =\'%s\''% (m[0].mii.unk1,j1[0].comment,str(convertPrincipalIdtoFriendCode(m[0].unk1)).zfill(12)))
98+
99+
con.commit()
100+
101+
time.sleep(delay)
102+
forfriendinrotation:
103+
awaitfriends_client.remove_friend_by_principal_id(friend)
103104
exceptRuntimeErrorase:
104105
print('An error occurred!\n%s'%e)
105106
break
106107
except:
107108
time.sleep(2)
108109

109110
if__name__=='__main__':
110-
anyio.run(main)
111+
try:
112+
anyio.run(main)
113+
exceptKeyboardInterrupt:
114+
startDBTime(0)

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp