Compare commits
No commits in common. "main" and "blitzwing-patch-1" have entirely different histories.
main
...
blitzwing-
5 changed files with 14 additions and 389 deletions
29
README.md
29
README.md
|
@ -19,30 +19,13 @@ This project is still in development and more features will be added in the futu
|
|||
|
||||
## Instances
|
||||
|
||||
| URL | Provided by | Country | Comments |
|
||||
| ----------------------------------------------------------------- | ----------------------------------------------- | ------------- | -------- |
|
||||
| [wikimore.private.coffee](https://wikimore.private.coffee/) | [Private.coffee](https://private.coffee/) | Austria 🇦🇹 🇪🇺 | |
|
||||
| [wm.bloat.cat](https://wm.bloat.cat/) | [bloat.cat](https://bloat.cat/) | Germany 🇩🇪 🇪🇺 | |
|
||||
| [wikimore.blitzw.in](https://wikimore.blitzw.in/) | [blitzw.in](https://blitzw.in/) | Denmark 🇩🇰 🇪🇺 | |
|
||||
| [wikimore.lumaeris.com](https://wikimore.lumaeris.com/) | [Lumaeris](https://lumaeris.com/) | Germany 🇩🇪 🇪🇺 | |
|
||||
| [wikimore.darkness.services](https://wikimore.darkness.services/) | [Darkness.services](https://darkness.services/) | USA 🇺🇸 | |
|
||||
| URL | Provided by | Country | Comments |
|
||||
| ----------------------------------------------------------- | ----------------------------------------- | ------- | -------- |
|
||||
| [wikimore.private.coffee](https://wikimore.private.coffee/) | [Private.coffee](https://private.coffee/) | Austria | |
|
||||
| [wm.bloat.cat](https://wm.bloat.cat/) | [bloat.cat](https://bloat.cat/) | Germany | |
|
||||
| [wikimore.blitzw.in](https://wikimore.blitzw.in/) | [blitzw.in](https://blitzw.in/) | Denmark | |
|
||||
|
||||
### Tor Hidden Services
|
||||
|
||||
| URL | Provided by | Country | Comments |
|
||||
| ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | ------------- | -------- |
|
||||
| [wikimore.coffee2m3bjsrrqqycx6ghkxrnejl2q6nl7pjw2j4clchjj6uk5zozad.onion](http://wikimore.coffee2m3bjsrrqqycx6ghkxrnejl2q6nl7pjw2j4clchjj6uk5zozad.onion/) | [Private.coffee](https://private.coffee/) | Austria 🇦🇹 🇪🇺 | |
|
||||
| [wikimore.darknessrdor43qkl2ngwitj72zdavfz2cead4t5ed72bybgauww5lyd.onion](http://wikimore.darknessrdor43qkl2ngwitj72zdavfz2cead4t5ed72bybgauww5lyd.onion/) | [Darkness.services](https://darkness.services/) | USA 🇺🇸 | |
|
||||
|
||||
### Adding Your Instance
|
||||
|
||||
To add your own instance to this list, please open a pull request or issue, see below.
|
||||
|
||||
## Opening Issues
|
||||
|
||||
If you're having problems using Wikimore, or if you have ideas or feedback for us, feel free to open an issue in the [Private.coffee Git](https://git.private.coffee/PrivateCoffee/wikimore/issues) or on [Github](https://github.com/PrivateCoffee/wikimore/issues).
|
||||
|
||||
Of course, you can also join our [Matrix room](https://matrix.pcof.fi/#/#wikimore:private.coffee) to discuss your ideas with us.
|
||||
If you operate a public instance of Wikimore and would like to have it listed here, please open an issue or a pull request.
|
||||
|
||||
## Installation
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||
|
||||
[project]
|
||||
name = "wikimore"
|
||||
version = "0.1.8"
|
||||
version = "0.1.5"
|
||||
authors = [{ name = "Private.coffee Team", email = "support@private.coffee" }]
|
||||
description = "A simple frontend for Wikimedia wikis"
|
||||
readme = "README.md"
|
||||
|
|
|
@ -123,96 +123,6 @@ logger.debug(
|
|||
)
|
||||
|
||||
|
||||
# Get number of active Wikipedia users for each language
|
||||
def get_active_users() -> Dict[str, int]:
|
||||
"""Fetch the number of active Wikipedia users for each language.
|
||||
|
||||
Returns:
|
||||
Dict[str, int]: A dictionary mapping language codes to the number of active Wikipedia users.
|
||||
"""
|
||||
path = "/w/api.php?action=query&format=json&meta=siteinfo&siprop=statistics"
|
||||
|
||||
active_users = {}
|
||||
|
||||
for lang, data in app.languages.items():
|
||||
try:
|
||||
url = f"{data['projects']['wiki']}{path}"
|
||||
with urllib.request.urlopen(url) as response:
|
||||
data = json.loads(response.read().decode())
|
||||
active_users[lang] = data["query"]["statistics"]["activeusers"]
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching active users for {lang}: {e}")
|
||||
|
||||
return sorted(active_users.items(), key=lambda x: x[1], reverse=True)
|
||||
|
||||
|
||||
if os.environ.get("NO_LANGSORT", False):
|
||||
LANGSORT = []
|
||||
elif os.environ.get("LANGSORT") == "auto":
|
||||
LANGSORT = [lang for lang, _ in get_active_users()[:50]]
|
||||
elif os.environ.get("LANGSORT"):
|
||||
LANGSORT = os.environ["LANGSORT"].split(",")
|
||||
else:
|
||||
# Opinionated sorting of languages
|
||||
LANGSORT = [
|
||||
"en",
|
||||
"es",
|
||||
"ja",
|
||||
"de",
|
||||
"fr",
|
||||
"zh",
|
||||
"ru",
|
||||
"it",
|
||||
"pt",
|
||||
"pl",
|
||||
"nl",
|
||||
"ar",
|
||||
]
|
||||
|
||||
|
||||
def langsort(input: list[dict], key: str = "lang") -> list[dict]:
|
||||
"""Sorting of language data.
|
||||
|
||||
Sorts a list of dictionaries containing "lang" keys such that the most common languages are first.
|
||||
|
||||
Allows specifying a custom order using the `LANGSORT` environment variable.
|
||||
|
||||
Args:
|
||||
input (list[dict]): A list of dictionaries containing "lang" keys.
|
||||
|
||||
Returns:
|
||||
list[dict]: The sorted list of dictionaries.
|
||||
"""
|
||||
|
||||
if not LANGSORT:
|
||||
return input
|
||||
|
||||
output = []
|
||||
|
||||
for lang in LANGSORT:
|
||||
for item in input:
|
||||
if item[key] == lang:
|
||||
output.append(item)
|
||||
|
||||
for item in input:
|
||||
if item[key] not in LANGSORT:
|
||||
output.append(item)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
logger.debug("Initialized language sort order")
|
||||
|
||||
app_languages = [
|
||||
{"lang": lang, "name": data["name"]} for lang, data in app.languages.items()
|
||||
]
|
||||
app_languages = langsort(app_languages)
|
||||
|
||||
app.languages = {
|
||||
lang: app.languages[lang] for lang in [lang["lang"] for lang in app_languages]
|
||||
}
|
||||
|
||||
|
||||
def render_template(*args, **kwargs) -> Text:
|
||||
"""A wrapper around Flask's `render_template` that adds the `languages` and `wikimedia_projects` context variables.
|
||||
|
||||
|
@ -333,16 +243,18 @@ def inbound_redirect(domain: str, url: str) -> Union[Text, Response, Tuple[Text,
|
|||
Returns:
|
||||
Response: A redirect to the corresponding route
|
||||
"""
|
||||
# TODO: Make this the default route scheme instead of a redirect
|
||||
|
||||
for language, language_projects in app.languages.items():
|
||||
for project_name, project_url in language_projects["projects"].items():
|
||||
if project_url == f"https://{domain}":
|
||||
return redirect(f"{url_for('home')}{project_name}/{language}/{url}")
|
||||
return redirect(
|
||||
f"{url_for('home')}{project_name}/{language}/{url}"
|
||||
)
|
||||
|
||||
for project_name, project_url in app.languages["special"]["projects"].items():
|
||||
if project_url == f"https://{domain}":
|
||||
return redirect(f"{url_for('home')}/{project_name}/{language}/{url}")
|
||||
return redirect(
|
||||
f"{url_for('home')}/{project_name}/{language}/{url}"
|
||||
)
|
||||
|
||||
# TODO / IDEA: Handle non-Wikimedia Mediawiki projects here?
|
||||
|
||||
|
@ -355,7 +267,6 @@ def inbound_redirect(domain: str, url: str) -> Union[Text, Response, Tuple[Text,
|
|||
404,
|
||||
)
|
||||
|
||||
|
||||
@app.route("/<project>/<lang>/wiki/<path:title>")
|
||||
def wiki_article(
|
||||
project: str, lang: str, title: str
|
||||
|
@ -392,146 +303,11 @@ def wiki_article(
|
|||
|
||||
logger.debug(f"Fetching {title} from {base_url}")
|
||||
|
||||
# Check if the article is something we need to handle differently
|
||||
info_api_request = urllib.request.Request(
|
||||
f"{base_url}/w/api.php?action=query&format=json&titles={escape(quote(title.replace(' ', '_')), True)}&prop=info|pageprops|categoryinfo|langlinks&lllimit=500",
|
||||
headers=HEADERS,
|
||||
)
|
||||
|
||||
category_members = []
|
||||
interwiki = []
|
||||
badges = []
|
||||
|
||||
with urllib.request.urlopen(info_api_request) as response:
|
||||
logger.debug(
|
||||
f"Tried to fetch info for {title} from {info_api_request.full_url}"
|
||||
)
|
||||
data = json.loads(response.read().decode())
|
||||
page = data["query"]["pages"].popitem()[1]
|
||||
|
||||
langlinks = page.get("langlinks", [])
|
||||
|
||||
logger.debug(f"Original Interwiki links for {title}: {langlinks}")
|
||||
|
||||
# Get interwiki links and translate them to internal links where possible
|
||||
for link in langlinks:
|
||||
try:
|
||||
interwiki_lang = link["lang"]
|
||||
interwiki_title = link["*"]
|
||||
|
||||
logger.debug(
|
||||
f"Generating interwiki link for: {interwiki_lang}.{project}/{interwiki_title}"
|
||||
)
|
||||
|
||||
interwiki_url = url_for(
|
||||
"wiki_article",
|
||||
project=project,
|
||||
lang=interwiki_lang,
|
||||
title=interwiki_title,
|
||||
)
|
||||
link["url"] = interwiki_url
|
||||
|
||||
link["langname"] = app.languages[interwiki_lang]["name"]
|
||||
|
||||
interwiki.append(link)
|
||||
|
||||
except KeyError as e:
|
||||
logger.error(
|
||||
f"Error processing interwiki link for title {title} in language {lang}: {e}"
|
||||
)
|
||||
|
||||
# Get badges (e.g. "Good Article", "Featured Article")
|
||||
props = page.get("pageprops", {})
|
||||
|
||||
for prop in props:
|
||||
if prop.startswith("wikibase-badge-"):
|
||||
try:
|
||||
badge_id = prop.replace("wikibase-badge-", "")
|
||||
|
||||
# Fetch the badge data from Wikidata
|
||||
badge_request = urllib.request.Request(
|
||||
f"https://www.wikidata.org/w/api.php?action=wbgetentities&format=json&ids={badge_id}&languages={lang}",
|
||||
headers=HEADERS,
|
||||
)
|
||||
|
||||
with urllib.request.urlopen(badge_request) as badge_response:
|
||||
logger.debug(
|
||||
f"Tried to fetch badge {badge_id} from {badge_request.full_url}"
|
||||
)
|
||||
|
||||
badge_data = json.loads(badge_response.read().decode())
|
||||
|
||||
badge = badge_data["entities"][badge_id]["labels"][lang][
|
||||
"value"
|
||||
]
|
||||
badge_image = badge_data["entities"][badge_id]["claims"]["P18"][
|
||||
0
|
||||
]["mainsnak"]["datavalue"]["value"]
|
||||
badges.append(
|
||||
{
|
||||
"title": badge,
|
||||
"url": f"https://www.wikidata.org/wiki/{badge_id}",
|
||||
"image": get_proxy_url(
|
||||
f"https://commons.wikimedia.org/wiki/Special:Redirect/file/{badge_image}"
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching badge {prop}: {e}")
|
||||
|
||||
# If the article is a category, fetch the category members
|
||||
if "categoryinfo" in page:
|
||||
category_api_url = f"{base_url}/w/api.php?action=query&format=json&list=categorymembers&cmtitle={escape(quote(title.replace(' ', '_')), True)}&cmlimit=500"
|
||||
|
||||
category_api_request = urllib.request.Request(
|
||||
category_api_url,
|
||||
headers=HEADERS,
|
||||
)
|
||||
|
||||
all_members = []
|
||||
|
||||
with urllib.request.urlopen(category_api_request) as category_api_response:
|
||||
logger.debug(
|
||||
f"Tried to fetch category members for {title} from {category_api_request.full_url}"
|
||||
)
|
||||
data = json.loads(category_api_response.read().decode())
|
||||
category_members = data["query"]["categorymembers"]
|
||||
all_members += category_members
|
||||
|
||||
if "continue" in data:
|
||||
continue_params = f"&cmcontinue={data['continue']['cmcontinue']}"
|
||||
category_api_request = urllib.request.Request(
|
||||
category_api_url + continue_params,
|
||||
headers=HEADERS,
|
||||
)
|
||||
|
||||
with urllib.request.urlopen(
|
||||
category_api_request
|
||||
) as category_api_response:
|
||||
data = json.loads(category_api_response.read().decode())
|
||||
all_members += data["query"]["categorymembers"]
|
||||
|
||||
category_members = all_members
|
||||
|
||||
for member in category_members:
|
||||
member["url"] = url_for(
|
||||
"wiki_article",
|
||||
project=project,
|
||||
lang=lang,
|
||||
title=member["title"],
|
||||
)
|
||||
|
||||
interwiki = langsort(interwiki)
|
||||
|
||||
# Prepare the API request to fetch the article content
|
||||
api_request = urllib.request.Request(
|
||||
f"{base_url}/api/rest_v1/page/html/{escape(quote(title.replace(' ', '_')), True).replace('/', '%2F')}",
|
||||
headers=HEADERS,
|
||||
)
|
||||
|
||||
logger.debug(f"Article content URL: {api_request.full_url}")
|
||||
|
||||
# Add the `variant` header if the `variant` query parameter is present
|
||||
# This is used to fetch articles in a specific script variant (https://www.mediawiki.org/wiki/Writing_systems/LanguageConverter)
|
||||
if request.args.get("variant", None):
|
||||
|
@ -652,9 +428,6 @@ def wiki_article(
|
|||
for img in soup.find_all("img"):
|
||||
img["src"] = get_proxy_url(img["src"])
|
||||
|
||||
# While we're at it, ensure that images are loaded lazily
|
||||
img["loading"] = "lazy"
|
||||
|
||||
for source in soup.find_all("source"):
|
||||
source["src"] = get_proxy_url(source["src"])
|
||||
|
||||
|
@ -714,9 +487,6 @@ def wiki_article(
|
|||
project=project,
|
||||
rtl=rtl,
|
||||
license=license,
|
||||
interwiki=interwiki,
|
||||
badges=badges,
|
||||
category_members=category_members,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -1228,95 +1228,4 @@ Currently blocked by implementation of comments retrieval in the backend
|
|||
.side-box-text {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
}
|
||||
|
||||
/* Language selector styling */
|
||||
.language-selector {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.language-selector-toggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.language-selector-label {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
padding: 0.5em;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 0.25em;
|
||||
}
|
||||
|
||||
.language-selector-label-text {
|
||||
display: inline-block;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.language-selector-label-icon {
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 0.25em solid transparent;
|
||||
border-right: 0.25em solid transparent;
|
||||
border-top: 0.25em solid #333;
|
||||
}
|
||||
|
||||
.language-selector-menu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
background-color: #fff;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 0.25em;
|
||||
}
|
||||
|
||||
.language-selector-toggle:checked + .language-selector-label + .language-selector-menu {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.language-selector-list {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.language-selector-item {
|
||||
border-top: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.language-selector-link {
|
||||
display: block;
|
||||
padding: 0.5em;
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.language-selector-link:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.language-selector-link:active {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
.language-selector-link:focus {
|
||||
outline: 1px dotted #333;
|
||||
outline: 5px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
.language-selector-link:active,
|
||||
.language-selector-link:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.language-selector-link:active,
|
||||
.language-selector-link:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.badge > img {
|
||||
max-height: 1em;
|
||||
}
|
|
@ -1,45 +1,8 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="title{% if rtl %} title-rtl{% endif %}">
|
||||
{{ title }}
|
||||
{% if badges %}
|
||||
<span class="badges">
|
||||
{% for badge in badges %}
|
||||
<a href="{{ badge.url }}" class="badge" title="{{ badge.title }}">
|
||||
<img src="{{ badge.image }}" alt="{{ badge.title }}">
|
||||
</a>
|
||||
{% endfor %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</h1>
|
||||
{% if interwiki %}
|
||||
<div class="language-selector">
|
||||
<input type="checkbox" id="language-selector-toggle" class="language-selector-toggle">
|
||||
<label for="language-selector-toggle" class="language-selector-label">
|
||||
<span class="language-selector-label-text">Language</span>
|
||||
<span class="language-selector-label-icon"></span>
|
||||
</label>
|
||||
<div class="language-selector-menu">
|
||||
<ul class="language-selector-list">
|
||||
{% for lang in interwiki %}
|
||||
<li class="language-selector-item">
|
||||
<a href="{{ lang.url }}" class="language-selector-link">{{ lang.langname }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<h1 class="title{% if rtl %} title-rtl{% endif %}">{{ title }}</h1>
|
||||
{{ content|safe }}
|
||||
{% if category_members %}
|
||||
<h2>Pages in category "{{ title }}"</h2>
|
||||
<ul>
|
||||
{% for member in category_members %}
|
||||
<li><a href="{{ member.url }}">{{ member.title }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block license %}
|
||||
|
|
Loading…
Reference in a new issue