Skip to content

Commit 5021d00

Browse files
committed
src/qml/options/ankiintegrationpage: add support for api keys
Add support for AnkiConnect API keys. This are optionally togglable by the user. This also forces the user to enter either http:// or https:// in the hostname so it is clear which protocol should be used. This removes all config file validation, instead just overwriting invalid values with defaults. This is to make adding new options easier in the future. Closes #324
1 parent aca0c77 commit 5021d00

7 files changed

Lines changed: 195 additions & 162 deletions

File tree

src/anki/ankiclient.cpp

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -855,15 +855,19 @@ QCoro::Task<std::unique_ptr<QNetworkReply>> AnkiClient::makeRequest(
855855
{
856856
QNetworkRequest request;
857857
request.setUrl(QUrl(
858-
QString("http://%1:%2")
858+
QString("%1:%2")
859859
.arg(profile->hostname())
860860
.arg(profile->port())
861861
));
862862
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
863863

864864
QJsonObject jsonMsg;
865865
jsonMsg[AnkiConnect::Req::ACTION] = action;
866-
jsonMsg[AnkiConnect::Reply::VERSION] = MIN_ANKICONNECT_VERSION;
866+
jsonMsg[AnkiConnect::Req::VERSION] = MIN_ANKICONNECT_VERSION;
867+
if (profile->useApiKey())
868+
{
869+
jsonMsg[AnkiConnect::Req::KEY] = profile->apiKey();
870+
}
867871
if (!params.isEmpty())
868872
{
869873
jsonMsg[AnkiConnect::Req::PARAMS] = params;

src/anki/ankiconfig.cpp

Lines changed: 26 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -78,159 +78,8 @@ bool AnkiConfig::load()
7878
return false;
7979
}
8080

81-
/* Error check the config */
81+
/* Get the config */
8282
QJsonObject jsonObj = jsonDoc.object();
83-
if (!jsonObj[Anki::Keys::ENABLED].isBool())
84-
{
85-
setError(
86-
tr(PREFIX_ERR_STR "\"%1\" is not a boolean")
87-
.arg(Anki::Keys::ENABLED)
88-
);
89-
return false;
90-
}
91-
else if (!jsonObj[Anki::Keys::SET_PROFILE].isString())
92-
{
93-
setError(
94-
tr(PREFIX_ERR_STR "\"%1\" is not a string")
95-
.arg(Anki::Keys::SET_PROFILE)
96-
);
97-
return false;
98-
}
99-
else if (!jsonObj[Anki::Keys::PROFILES].isArray())
100-
{
101-
setError(
102-
tr(PREFIX_ERR_STR "\"%1\" is not an array")
103-
.arg(Anki::Keys::PROFILES)
104-
);
105-
return false;
106-
}
107-
QJsonArray profiles = jsonObj[Anki::Keys::PROFILES].toArray();
108-
for (qsizetype i = 0; i < profiles.size(); ++i)
109-
{
110-
if (!profiles[i].isObject())
111-
{
112-
setError(
113-
tr(PREFIX_ERR_STR "\"%1\" element is not a JSON object")
114-
.arg(Anki::Keys::PROFILES)
115-
);
116-
return false;
117-
}
118-
QJsonObject profile = profiles[i].toObject();
119-
120-
/* Error check values */
121-
if (!profile[Anki::Keys::NAME].isString())
122-
{
123-
setError(
124-
tr(PREFIX_ERR_STR "\"%1\" is not a string")
125-
.arg(Anki::Keys::NAME)
126-
);
127-
return false;
128-
}
129-
else if (!profile[Anki::Keys::HOSTNAME].isString())
130-
{
131-
setError(
132-
tr(PREFIX_ERR_STR "\"%1\" is not a string")
133-
.arg(Anki::Keys::HOSTNAME)
134-
);
135-
return false;
136-
}
137-
else if (!profile[Anki::Keys::PORT].isString())
138-
{
139-
setError(
140-
tr(PREFIX_ERR_STR "\"%1\" is not a string")
141-
.arg(Anki::Keys::PORT)
142-
);
143-
return false;
144-
}
145-
else if (!profile[Anki::Keys::DUPLICATE_POLICY].isDouble())
146-
{
147-
setError(
148-
tr(PREFIX_ERR_STR "\"%1\" is not a double")
149-
.arg(Anki::Keys::DUPLICATE_POLICY)
150-
);
151-
return false;
152-
}
153-
else if (!profile[Anki::Keys::NEWLINE_REPLACER].isString())
154-
{
155-
setError(
156-
tr(PREFIX_ERR_STR "\"%1\" is not a string")
157-
.arg(Anki::Keys::NEWLINE_REPLACER)
158-
);
159-
return false;
160-
}
161-
else if (!profile[Anki::Keys::SCREENSHOT].isDouble())
162-
{
163-
setError(
164-
tr(PREFIX_ERR_STR "\"%1\" is not a double")
165-
.arg(Anki::Keys::SCREENSHOT)
166-
);
167-
return false;
168-
}
169-
else if (!profile[Anki::Keys::AUDIO_PAD_START].isDouble())
170-
{
171-
setError(
172-
tr(PREFIX_ERR_STR "\"%1\" is not a double")
173-
.arg(Anki::Keys::AUDIO_PAD_START)
174-
);
175-
return false;
176-
}
177-
else if (!profile[Anki::Keys::AUDIO_PAD_END].isDouble())
178-
{
179-
setError(
180-
tr(PREFIX_ERR_STR "\"%1\" is not a double")
181-
.arg(Anki::Keys::AUDIO_PAD_END)
182-
);
183-
return false;
184-
}
185-
else if (!profile[Anki::Keys::AUDIO_NORMALIZE].isBool())
186-
{
187-
setError(
188-
tr(PREFIX_ERR_STR "\"%1\" is not a bool")
189-
.arg(Anki::Keys::AUDIO_NORMALIZE)
190-
);
191-
return false;
192-
}
193-
else if (!profile[Anki::Keys::AUDIO_DB].isDouble())
194-
{
195-
setError(
196-
tr(PREFIX_ERR_STR "\"%1\" is not a double")
197-
.arg(Anki::Keys::AUDIO_DB)
198-
);
199-
return false;
200-
}
201-
else if (!profile[Anki::Keys::TAGS].isArray())
202-
{
203-
setError(
204-
tr(PREFIX_ERR_STR "\"%1\" is not an array")
205-
.arg(Anki::Keys::TAGS)
206-
);
207-
return false;
208-
}
209-
else if (!profile[Anki::Keys::TERM].isObject())
210-
{
211-
setError(
212-
tr(PREFIX_ERR_STR "\"%1\" is not an object")
213-
.arg(Anki::Keys::TERM)
214-
);
215-
return false;
216-
}
217-
else if (!profile[Anki::Keys::KANJI].isObject())
218-
{
219-
setError(
220-
tr(PREFIX_ERR_STR "\"%1\" is not an object")
221-
.arg(Anki::Keys::KANJI)
222-
);
223-
return false;
224-
}
225-
else if (!profile[Anki::Keys::EXCLUDE_GLOSSARIES].isArray())
226-
{
227-
setError(
228-
tr(PREFIX_ERR_STR "\"%1\" is not an array")
229-
.arg(Anki::Keys::EXCLUDE_GLOSSARIES)
230-
);
231-
return false;
232-
}
233-
}
23483

23584
/* Clear out existing profiles */
23685
qDeleteAll(m_profiles);
@@ -241,20 +90,37 @@ bool AnkiConfig::load()
24190
m_enabled = jsonObj[Anki::Keys::ENABLED]
24291
.toBool(Anki::Keys::ENABLED_DEFAULT);
24392
QString currentProfile = jsonObj[Anki::Keys::SET_PROFILE].toString();
93+
QJsonArray profiles = jsonObj[Anki::Keys::PROFILES].toArray();
24494
for (const QJsonValue &val : profiles)
24595
{
24696
QJsonObject profile = val.toObject();
24797

24898
AnkiProfile *ankiProfile = new AnkiProfile(this);
24999
ankiProfile->setName(
250-
profile[Anki::Keys::NAME].toString(Anki::Keys::NAME_DEFAULT)
100+
profile[Anki::Keys::NAME]
101+
.toString(Anki::Keys::NAME_DEFAULT)
251102
);
252103
ankiProfile->setHostname(
253104
profile[Anki::Keys::HOSTNAME]
254105
.toString(Anki::Keys::HOSTNAME_DEFAULT)
255106
);
107+
/* Make sure that hostnames include http:// or https:// */
108+
if (!ankiProfile->hostname().startsWith("http://") &&
109+
!ankiProfile->hostname().startsWith("https://"))
110+
{
111+
ankiProfile->setHostname("http://" + ankiProfile->hostname());
112+
}
256113
ankiProfile->setPort(
257-
profile[Anki::Keys::PORT].toString(Anki::Keys::PORT)
114+
profile[Anki::Keys::PORT]
115+
.toString(Anki::Keys::PORT_DEFAULT)
116+
);
117+
ankiProfile->setUseApiKey(
118+
profile[Anki::Keys::USE_API_KEY]
119+
.toBool(Anki::Keys::USE_API_KEY_DEFAULT)
120+
);
121+
ankiProfile->setApiKey(
122+
profile[Anki::Keys::API_KEY]
123+
.toString(Anki::Keys::API_KEY_DEFAULT)
258124
);
259125
ankiProfile->setDuplicatePolicy(static_cast<Anki::DuplicatePolicy>(
260126
profile[Anki::Keys::DUPLICATE_POLICY]
@@ -332,6 +198,10 @@ bool AnkiConfig::load()
332198
return lhs->name() < rhs->name();
333199
}
334200
);
201+
if (m_profiles.isEmpty())
202+
{
203+
m_profiles.emplaceBack(new AnkiProfile{this});
204+
}
335205
if (m_profile == nullptr)
336206
{
337207
m_profile = m_profiles.first();
@@ -377,6 +247,8 @@ bool AnkiConfig::write()
377247
configObj[Anki::Keys::NAME] = profile->name();
378248
configObj[Anki::Keys::HOSTNAME] = profile->hostname();
379249
configObj[Anki::Keys::PORT] = profile->port();
250+
configObj[Anki::Keys::USE_API_KEY] = profile->useApiKey();
251+
configObj[Anki::Keys::API_KEY] = profile->apiKey();
380252
configObj[Anki::Keys::DUPLICATE_POLICY] = profile->duplicatePolicy();
381253
configObj[Anki::Keys::NEWLINE_REPLACER] = profile->newlineReplacer();
382254
configObj[Anki::Keys::SCREENSHOT] = profile->screenshotType();

src/anki/ankiconfigkeys.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,17 @@ constexpr const char *NAME = "name";
7171
constexpr const char *NAME_DEFAULT = "Default";
7272

7373
constexpr const char *HOSTNAME = "host";
74-
constexpr const char *HOSTNAME_DEFAULT = "localhost";
74+
constexpr const char *HOSTNAME_DEFAULT = "http://localhost";
7575

7676
constexpr const char *PORT = "port";
7777
constexpr const char *PORT_DEFAULT = "8765";
7878

79+
constexpr const char *USE_API_KEY = "use-api-key";
80+
constexpr bool USE_API_KEY_DEFAULT = false;
81+
82+
constexpr const char *API_KEY = "api-key";
83+
constexpr const char *API_KEY_DEFAULT = "";
84+
7985
constexpr const char *DUPLICATE_POLICY = "duplicate";
8086
constexpr Anki::DuplicatePolicy DUPLICATE_POLICY_DEFAULT =
8187
Anki::DuplicatePolicyDifferentDeck;

src/anki/ankiconnect.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ namespace AnkiConnect
2828
namespace Req
2929
{
3030
constexpr const char *ACTION = "action";
31-
constexpr const char *RESULT = "result";
31+
constexpr const char *KEY = "key";
3232
constexpr const char *PARAMS = "params";
33+
constexpr const char *RESULT = "result";
34+
constexpr const char *VERSION = "version";
3335
}
3436

3537
namespace Reply

src/anki/ankiprofile.cpp

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ AnkiProfile *AnkiProfile::clone(QObject *parent) const
3131
copy->setName(name());
3232
copy->setHostname(hostname());
3333
copy->setPort(port());
34+
copy->setUseApiKey(useApiKey());
35+
copy->setApiKey(apiKey());
3436
copy->setDuplicatePolicy(duplicatePolicy());
3537
copy->setNewlineReplacer(newlineReplacer());
3638
copy->setScreenshotType(screenshotType());
@@ -53,6 +55,8 @@ void AnkiProfile::defaults()
5355
{
5456
setHostname();
5557
setPort();
58+
setUseApiKey();
59+
setApiKey();
5660
setDuplicatePolicy();
5761
setNewlineReplacer();
5862
setScreenshotType();
@@ -111,6 +115,36 @@ void AnkiProfile::setPort(const QString &value)
111115
emit portChanged(m_port);
112116
}
113117

118+
bool AnkiProfile::useApiKey() const noexcept
119+
{
120+
return m_useApiKey;
121+
}
122+
123+
void AnkiProfile::setUseApiKey(bool value)
124+
{
125+
if (m_useApiKey == value)
126+
{
127+
return;
128+
}
129+
m_useApiKey = value;
130+
emit useApiKeyChanged(m_useApiKey);
131+
}
132+
133+
const QString &AnkiProfile::apiKey() const noexcept
134+
{
135+
return m_apiKey;
136+
}
137+
138+
void AnkiProfile::setApiKey(const QString &value)
139+
{
140+
if (m_apiKey == value)
141+
{
142+
return;
143+
}
144+
m_apiKey = value;
145+
emit apiKeyChanged(m_apiKey);
146+
}
147+
114148
Anki::DuplicatePolicy AnkiProfile::duplicatePolicy() const noexcept
115149
{
116150
return m_duplicatePolicy;

0 commit comments

Comments
 (0)