Compare commits

..

No commits in common. "main" and "blitzwing-patch-1" have entirely different histories.

5 changed files with 14 additions and 389 deletions

View file

@ -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

View file

@ -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"

View file

@ -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,
)

View file

@ -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;
}

View file

@ -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 %}