-
Notifications
You must be signed in to change notification settings - Fork 96
Expand file tree
/
Copy pathupdate.py
More file actions
executable file
·212 lines (175 loc) · 8.9 KB
/
Copy pathupdate.py
File metadata and controls
executable file
·212 lines (175 loc) · 8.9 KB
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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
#!/usr/bin/env python3
import datetime
import glob
import os
import re
import subprocess
import sys
import io
import zipfile
# check dependencies are available
try:
import requests
except ImportError as e:
sys.exit(f"Error: {str(e)}. Try 'python3 -m pip install --user <module-name>' (or 'python3-requests' from Debian)")
try:
import polib
except ImportError as e:
sys.exit(f"Error: {str(e)}. Try 'python3 -m pip install --user <module-name>' (or 'python3-polib' from Debian)")
try:
subprocess.check_output(["msgattrib", "--version"])
except (subprocess.CalledProcessError, OSError):
raise Exception("missing gettext. Maybe try 'apt install gettext'")
crowdin_project_id = 20482 # for "Electrum" project on crowdin
BITCOIN_ADDRESS_REGEXP = re.compile('([13]|bc1)[a-zA-Z0-9]{30,}')
EMAIL_ADDRESS_REGEXP = re.compile(r'[\w.+-]+@[\w-]+\.[\w.-]+')
URL1_REGEXP = re.compile(r"\S+\.\S*\w+\S*/\S+") # str has a "." and somewhere later has a "/" ==> URL?
assert URL1_REGEXP.search("download security update from scamwebsite.com/click-here") is not None
URL2_REGEXP = re.compile(r"http(s){0,1}://")
MIXED_LETTERS_AND_DIGITS_WORD_REGEXP = re.compile( # try to match any cryptocurrency address
r"(?a)" # limit match to ascii, as CJK languages do not put whitespaces between words (would match full sentence otherwise)
r"(?=\w{16,})" # positive lookahead: check word length >=16
r"("
r"(\w*[a-zA-Z]\w*[0-9]\w*)|" # contains a letter and then later a digit, OR
r"(\w*[0-9]\w*[a-zA-Z]\w*)" # contains a digit and then later a letter
r")"
)
assert not (MIXED_LETTERS_AND_DIGITS_WORD_REGEXP.search("bip39 seeds cannot be converted to electrum seeds") is not None)
assert not (MIXED_LETTERS_AND_DIGITS_WORD_REGEXP.search("ライトニングは現在p2wpkhアドレスのHDウォレットでのみ利用可能です。") is not None)
assert MIXED_LETTERS_AND_DIGITS_WORD_REGEXP.search("ラ TB1Q9DVVAG8NS2EPXZ285FW03CR78HWEJP0Z7DFAFE") is not None
assert MIXED_LETTERS_AND_DIGITS_WORD_REGEXP.search("bitcoin:tb1q9dvvag8ns2epxz285fw03cr78hwejp0z7dfafe?amount=12345678") is not None
assert MIXED_LETTERS_AND_DIGITS_WORD_REGEXP.search("0x456d9347342B72BCf800bBf117391ac2f807c6bF") is not None # eth
assert MIXED_LETTERS_AND_DIGITS_WORD_REGEXP.search("84EgZVjXKF4d1JkEhZSxm4LQQEx64AvqQEwkvWPtHEb5JMrB1Y86y1vCPSCiXsKzbfS9x8vCpx3gVgPaHCpobPYqQzANTnC") is not None # xmr
def get_crowdin_api_key() -> str:
crowdin_api_key = None
if "crowdin_api_key" in os.environ:
return os.environ["crowdin_api_key"]
filename = os.path.expanduser('~/.crowdin_api_key')
if os.path.exists(filename):
with open(filename) as f:
crowdin_api_key = f.read().strip()
return crowdin_api_key
def pull_locale(path, *, crowdin_api_key=None):
global_headers = {}
if crowdin_api_key is None:
crowdin_api_key = get_crowdin_api_key()
if not crowdin_api_key:
# Looks like crowdin does not even allow downloading without auth anymore.
raise Exception("missing required crowdin_api_key")
if crowdin_api_key:
global_headers["Authorization"] = "Bearer {}".format(crowdin_api_key)
if not os.path.exists(path):
os.mkdir(path)
os.chdir(path)
# note: We won't request a build now, instead we download the latest build.
# This assumes that the push_locale script was run recently (in the past few days).
print('Getting list of builds from crowdin...')
# https://support.crowdin.com/developer/api/v2/?q=api#tag/Translations/operation/api.projects.translations.builds.getMany
url = f'https://api.crowdin.com/api/v2/projects/{crowdin_project_id}/translations/builds'
headers = {**global_headers, **{"content-type": "application/json"}}
response = requests.request("GET", url, headers=headers)
response.raise_for_status()
print("", "translations.builds.getMany:", "-" * 20, response.text, "-" * 20, sep="\n")
latest_build = response.json()["data"][0]["data"]
assert latest_build["status"] == "finished", latest_build["status"]
# if latest_build["attributes"]["exportApprovedOnly"] is not True:
# raise Exception("latest_build from crowdin MUST have exportApprovedOnly==true")
created_at = datetime.datetime.fromisoformat(latest_build["createdAt"])
if (datetime.datetime.now(datetime.timezone.utc) - created_at) > datetime.timedelta(days=2):
raise Exception(f"latest translation build looks too old. {created_at.isoformat()=}")
build_id = latest_build["id"]
print('Asking crowdin to generate a URL for the latest build...')
# https://support.crowdin.com/developer/api/v2/?q=api#tag/Translations/operation/api.projects.translations.builds.download.download
url = f'https://api.crowdin.com/api/v2/projects/{crowdin_project_id}/translations/builds/{build_id}/download'
headers = {**global_headers, **{"content-type": "application/json"}}
response = requests.request("GET", url, headers=headers)
response.raise_for_status()
print("", "translations.builds.download.download:", "-" * 20, response.text, "-" * 20, sep="\n")
build_url = response.json()["data"]["url"]
# Download & unzip
print('Downloading translations...')
response = requests.request('GET', build_url, headers={})
response.raise_for_status()
s = response.content
zfobj = zipfile.ZipFile(io.BytesIO(s))
print('Unzipping translations...')
prefix = "electrum-client/locale/"
for name in zfobj.namelist():
if not name.startswith(prefix) or name == prefix:
continue
if name.endswith('/'):
if not os.path.exists(name[len(prefix):]):
os.mkdir(name[len(prefix):])
else:
name_suffix = name[len(prefix):]
with open(name_suffix, 'wb') as output:
output.write(zfobj.read(name))
if name.endswith('.po'):
filter_exclude_comment_lines(name_suffix)
filter_exclude_untranslated_strings(name_suffix)
else:
raise Exception(f"unexpected file inside zipfile from crowdin: {name}")
def filter_exclude_comment_lines(fname: str):
"""Remove lines starting with "#". ==> easier to review diffs
As auto-generated comments contain line numbers, removing them makes
reviewing diffs much more practical.
note: we could be more relaxed and only rm lines starting with "#:",
see https://www.gnu.org/software/gettext/manual/html_node/PO-File-Entries.html
"""
os.rename(fname, f"{fname}.orig")
with open(f"{fname}.orig", "r+", encoding="utf-8") as f_orig:
with open(fname, "w", encoding="utf-8") as f_filtered:
for line in f_orig:
if not line.startswith("#"):
f_filtered.write(line)
os.remove(f"{fname}.orig")
def filter_exclude_untranslated_strings(fname: str):
"""Remove strings with empty translations. ==> easier to review diffs"""
cmd = ["msgattrib", "--translated", "--no-wrap", "--output-file", fname, fname]
subprocess.check_output(cmd)
def detect_malicious_stuff_in_dir(path_locale: str) -> None:
is_detected = False
# scan each .po file separately
files_list = glob.glob(f"{path_locale}/**/*.po", recursive=True)
for fname in files_list:
is_detected |= detect_malicious_stuff_in_po_file(fname)
# after finding all errors, exit now if there were any:
if is_detected:
raise Exception("detected some possibly malicious translations. see logs above.")
def detect_malicious_stuff_in_po_file(fname: str) -> bool:
pofile = polib.pofile(fname)
is_detected = False
regexes = {
"BITCOIN_ADDRESS_REGEXP": BITCOIN_ADDRESS_REGEXP,
"EMAIL_ADDRESS_REGEXP": EMAIL_ADDRESS_REGEXP,
"URL1_REGEXP": URL1_REGEXP,
"URL2_REGEXP": URL2_REGEXP,
"MIXED_LETTERS_AND_DIGITS_WORD_REGEXP": MIXED_LETTERS_AND_DIGITS_WORD_REGEXP,
}
for entry in pofile:
for regex_name, regex in regexes.items():
if regex.search(entry.msgstr) is not None:
print(
f">> regex {regex_name} matched in {fname!r},\n"
f"\t{entry.msgid=!r}\n"
f"\t{entry.msgstr=!r}\n")
is_detected = True
return is_detected
if __name__ == '__main__':
path_here = os.path.dirname(os.path.realpath(__file__))
path_locale = os.path.join(path_here, "locale")
pull_locale(path_locale)
detect_malicious_stuff_in_dir(path_locale)
print('Local updates done.')
print("Please don't commit directly to master, switch to a branch instead.")
c = input("Do you want to git commit this? (y/n): ")
if c != "y":
sys.exit(0)
print('Preparing git commit...')
os.chdir(path_here)
for lang in os.listdir('locale'):
po = 'locale/%s/electrum.po' % lang
cmd = "git add %s"%po
os.system(cmd)
os.system("git commit -a -m 'update translations'")
print("please push to a branch, and open a pull request")