Skip to content

Commit

Permalink
[Qwant] Brand Suggest
Browse files Browse the repository at this point in the history
  • Loading branch information
jeromeboursier committed Mar 13, 2024
1 parent d1d4433 commit 9d33d64
Show file tree
Hide file tree
Showing 12 changed files with 671 additions and 50 deletions.
15 changes: 15 additions & 0 deletions BrowserKit/Sources/Common/Extensions/QwantExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,21 @@ public extension WKWebView {
configuration.websiteDataStore.httpCookieStore.setCookie(omnibarCookie)
configuration.websiteDataStore.httpCookieStore.setCookie(trackingCookie)
}

func abTestGroupLookup(_ completion: @escaping (Int?) -> Void) {
configuration.websiteDataStore.httpCookieStore.getAllCookies { cookies in
guard let value = cookies
.filter({ $0.domain.contains("qwant.com") })
.first(where: { $0.name == "ab_test_group" })?
.value
else {
completion(nil)
return
}

completion(Int(value))
}
}
}

public extension String {
Expand Down
9 changes: 9 additions & 0 deletions BrowserKit/Sources/Common/Theming/QwantDesignSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,5 +76,14 @@ public struct QwantUX {
/// Border width for a button is 1
public static let borderWidth: CGFloat = 1
}

public struct Favicon {
/// Height for a favicon image is 28
public static let height: CGFloat = 28
/// Corner radius for a favicon image is 5
public static let cornerRadius: CGFloat = 5
/// Border width for a favicon image is .5
public static let borderWidth: CGFloat = 0.5
}
}
}
8 changes: 8 additions & 0 deletions firefox-ios/Client.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,8 @@
68F2F49A2B9F573A0023A7FE /* QwantTracking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68F2F4992B9F573A0023A7FE /* QwantTracking.swift */; };
68F2F49C2B9F59B90023A7FE /* SendQwantTrackingSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68F2F49B2B9F59B90023A7FE /* SendQwantTrackingSetting.swift */; };
68F2F49F2B9F5BEF0023A7FE /* MockQwantTracking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68F2F49D2B9F5A790023A7FE /* MockQwantTracking.swift */; };
68F2F4A12BA06A670023A7FE /* QwantBrandSuggestClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68F2F4A02BA06A670023A7FE /* QwantBrandSuggestClient.swift */; };
68F2F4A32BA06BD30023A7FE /* QwantBrandSuggestCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68F2F4A22BA06BD30023A7FE /* QwantBrandSuggestCell.swift */; };
6A3E5D8A283831D1001E706E /* DownloadQueueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A3E5D89283831D0001E706E /* DownloadQueueTests.swift */; };
6A5F591D28627C0100FABA92 /* TabManagerNavDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A5F591C28627C0100FABA92 /* TabManagerNavDelegateTests.swift */; };
6ACB550C28633860007A6ABD /* TabManagerNavDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ACB550B28633860007A6ABD /* TabManagerNavDelegate.swift */; };
Expand Down Expand Up @@ -2681,6 +2683,8 @@
68F2F4992B9F573A0023A7FE /* QwantTracking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QwantTracking.swift; sourceTree = "<group>"; };
68F2F49B2B9F59B90023A7FE /* SendQwantTrackingSetting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendQwantTrackingSetting.swift; sourceTree = "<group>"; };
68F2F49D2B9F5A790023A7FE /* MockQwantTracking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockQwantTracking.swift; sourceTree = "<group>"; };
68F2F4A02BA06A670023A7FE /* QwantBrandSuggestClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QwantBrandSuggestClient.swift; sourceTree = "<group>"; };
68F2F4A22BA06BD30023A7FE /* QwantBrandSuggestCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QwantBrandSuggestCell.swift; sourceTree = "<group>"; };
6A3E5D89283831D0001E706E /* DownloadQueueTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadQueueTests.swift; sourceTree = "<group>"; };
6A5F591C28627C0100FABA92 /* TabManagerNavDelegateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabManagerNavDelegateTests.swift; sourceTree = "<group>"; };
6ACB550B28633860007A6ABD /* TabManagerNavDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabManagerNavDelegate.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -6763,6 +6767,7 @@
D863C8E31F68BFC20058D95F /* GradientProgressBar.swift */,
0BF1B7E21AC60DEA00A7B407 /* InsetButton.swift */,
D0152244229855A8009DE753 /* OneLineTableViewCell.swift */,
68F2F4A22BA06BD30023A7FE /* QwantBrandSuggestCell.swift */,
687E22572B9F128A009D9411 /* QwantSearchTableViewHeader.swift */,
D38A1BEC1A9FA2CA00F6A386 /* SiteTableViewController.swift */,
E16E1C9728C25F1D00EE2EF5 /* SiteTableViewHeader.swift */,
Expand Down Expand Up @@ -6873,6 +6878,7 @@
D34510871ACF415700EC27F0 /* SearchLoader.swift */,
D31A0FC61A65D6D000DC8C7E /* SearchSuggestClient.swift */,
23D57E6D25ED6F2700883FAD /* SearchViewController.swift */,
68F2F4A02BA06A670023A7FE /* QwantBrandSuggestClient.swift */,
687E22402B9B26B7009D9411 /* QwantSearchViewController.swift */,
3BF56D261CDBBE1F00AC4D75 /* SimpleToast.swift */,
E68AEDAF1B18F81A00133D99 /* SwipeAnimator.swift */,
Expand Down Expand Up @@ -9526,6 +9532,7 @@
8AD40FCF27BADC6B00672675 /* URLTextField.swift in Sources */,
31ADB5DA1E58CEC300E87909 /* ClipboardBarDisplayHandler.swift in Sources */,
8AD1980F27BEB3F100D64B0E /* PhotonActionSheetViewModel.swift in Sources */,
68F2F4A32BA06BD30023A7FE /* QwantBrandSuggestCell.swift in Sources */,
EB9A179D20E69A7F00B12184 /* LegacyTheme.swift in Sources */,
2128E2802934FBB400FB91BE /* CopyLinkActivity.swift in Sources */,
E17BE4C42A94BA6900C5124E /* FakespotHighlightGroupView.swift in Sources */,
Expand Down Expand Up @@ -9602,6 +9609,7 @@
8AF99B4F29EF1BA700108DEC /* BrowserDelegate.swift in Sources */,
C87D8B802818333F00A6307D /* NimbusManager.swift in Sources */,
8A4AC0EB28C929D700439F83 /* URLSessionDataTaskProtocol.swift in Sources */,
68F2F4A12BA06A670023A7FE /* QwantBrandSuggestClient.swift in Sources */,
C45F44691D087DB600CB7EF0 /* TopTabsViewController.swift in Sources */,
8A0727462B4890B50071BB9F /* WebviewTelemetry.swift in Sources */,
8ADC2A212A3399DC00543DAA /* YourRightsSetting.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1230,7 +1230,8 @@ class BrowserViewController: UIViewController,
let searchViewModel = SearchViewModel(isPrivate: isPrivate, isBottomSearchBar: isBottomSearchBar)
let searchController = QwantSearchViewController(profile: profile,
viewModel: searchViewModel,
tabManager: tabManager)
tabManager: tabManager,
qwantTracking: qwantTracking)
searchController.searchDelegate = self

let searchLoader = SearchLoader(profile: profile, urlBar: urlBar)
Expand Down Expand Up @@ -2340,6 +2341,10 @@ extension BrowserViewController: LegacyTabDelegate {
)
webView.uiDelegate = self
webView.setQwantCookies(tracking: profile.prefs.boolForKey(AppConstants.prefQwantTracking) ?? true)
webView.abTestGroupLookup { [weak self] abTest in
guard let abTest, let self else { return }
self.profile.prefs.setInt(Int32(abTest), forKey: PrefsKeys.QwantABTestGroup)
}

let formPostHelper = FormPostHelper(tab: tab)
tab.addContentScript(formPostHelper, name: FormPostHelper.name())
Expand Down
214 changes: 214 additions & 0 deletions firefox-ios/Client/Frontend/Browser/QwantBrandSuggestClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/

import Foundation
import Shared

struct QwantSuggest {
let title: String
let url: URL?
let faviconUrl: URL?
let domain: String
let brand: String
let suggestType: Int?
let query: String
let adPosition: Int

var isBrand: Bool {
return self.suggestType != nil && self.suggestType == 18 // ¯\_(ツ)_/¯
}

func toRequestBody(locale: String, abTestGroup: Int) -> [String: Any]? {
guard let suggestType,
let url,
let language = locale.split(separator: "_").first
else { return nil }

return [
"client": "qwantbrowser",
"data": [
"ad_type": "brand-suggest",
"ad_version": "customadserver",
"brand": brand,
"count": query.count,
"device": "smartphone",
"locale": locale,
"position": adPosition,
"query": query,
"suggest_type": suggestType,
"tgp": abTestGroup,
"type": "ad",
"url": url.absoluteString
],
"interface_language": language,
"tgp": abTestGroup,
"uri": ""
]
}
}

extension QwantSuggest {
init(title: String) {
self.init(title: title,
url: nil,
faviconUrl: nil,
domain: "",
brand: "",
suggestType: 0,
query: "",
adPosition: 0)
}
}

class QwantBrandSuggestClient {
private let suggestTemplate = "https://api.qwant.com/v3/suggest?q={query}&locale={locale}&version=2"
private let queryComponent = "{query}"
private let localeComponent = "{locale}"
private let maxBrandSuggestCount = 2

private var task: URLSessionTask?

fileprivate lazy var urlSession: URLSession = makeURLSession(
userAgent: UserAgent.getUserAgent(),
configuration: .ephemeral
)

func query(_ query: String, callback: @escaping (_ response: [QwantSuggest]?, _ error: NSError?) -> Void) {
let url = getURLFromTemplate(suggestTemplate, query: query)
if url == nil {
let error = NSError(
domain: SearchSuggestClientErrorDomain,
code: SearchSuggestClientErrorInvalidEngine,
userInfo: nil
)
callback(nil, error)
return
}

task = urlSession.dataTask(with: url!) { [weak self] (data, response, error) in
guard let self else {
let error = NSError(domain: SearchSuggestClientErrorDomain, code: -1, userInfo: nil)
callback(nil, error)
return
}

if let error = error {
callback(nil, error as NSError?)
return
}

guard let data = data,
validatedHTTPResponse(response, statusCode: 200..<300) != nil
else {
let error = NSError(
domain: SearchSuggestClientErrorDomain,
code: SearchSuggestClientErrorInvalidResponse,
userInfo: nil
)
callback(nil, error as NSError?)
return
}

let decoder = JSONDecoder()
// swiftlint: disable force_try
let suggest = try! decoder.decode(Suggest.self, from: data)
// swiftlint: enable force_try

var suggestions = [QwantSuggest]()
var adPosition = 0
for brandSuggest in suggest.data.special.prefix(self.maxBrandSuggestCount) {
guard
let brandUrl = brandSuggest.url,
let url = URL(string: brandUrl),
let brandFaviconUrl = brandSuggest.faviconURL,
let faviconUrl = URL(string: brandFaviconUrl)
else { continue }
adPosition += 1
suggestions.append(QwantSuggest(title: brandSuggest.name,
url: url,
faviconUrl: faviconUrl,
domain: brandSuggest.domain,
brand: brandSuggest.brand,
suggestType: brandSuggest.suggestType,
query: query,
adPosition: adPosition))
}

for regularSuggest in suggest.data.items {
suggestions.append(QwantSuggest(title: regularSuggest.value))
}

if suggestions.isEmpty {
let error = NSError(
domain: SearchSuggestClientErrorDomain,
code: SearchSuggestClientErrorInvalidResponse,
userInfo: nil
)
callback(nil, error)
return
}
callback(suggestions, nil)
}
task?.resume()
}

func cancelPendingRequest() {
task?.cancel()
}

private func getURLFromTemplate(_ searchTemplate: String, query: String) -> URL? {
if let escapedQuery = query.addingPercentEncoding(withAllowedCharacters: .SearchTermsAllowed) {
// Escape the search template as well in case it contains not-safe characters like symbols
let templateAllowedSet = NSMutableCharacterSet()
templateAllowedSet.formUnion(with: .URLAllowed)

// Allow brackets since we use them in our template as our insertion point
templateAllowedSet.formUnion(with: CharacterSet(charactersIn: "{}"))

if let encodedSearchTemplate = searchTemplate
.addingPercentEncoding(withAllowedCharacters: templateAllowedSet as CharacterSet) {
let localeString = Locale.current.identifier
let urlString = encodedSearchTemplate
.replacingOccurrences(of: queryComponent, with: escapedQuery, options: .literal, range: nil)
.replacingOccurrences(of: localeComponent, with: localeString, options: .literal, range: nil)
return URL(string: urlString)
}
}

return nil
}
}

// MARK: - Suggest
private struct Suggest: Codable {
let status: String
let data: DataClass
}

// MARK: - DataClass
private struct DataClass: Codable {
let items: [Item]
let special: [Special]
}

// MARK: - Item
private struct Item: Codable {
let value: String
let suggestType: Int
}

// MARK: - Special
private struct Special: Codable {
let type: String
let suggestType: Int
let name, domain: String
let url: String?
let brand: String
let faviconURL: String?

enum CodingKeys: String, CodingKey {
case type, suggestType, name, domain, url, brand
case faviconURL = "favicon_url"
}
}
Loading

0 comments on commit 9d33d64

Please sign in to comment.