User:Uo4tie/bot

From OpenStreetMap Wiki
Jump to navigation Jump to search

Python code to copy the informations in the sidebar of the Key:* and Tag:* description pages to the data item via MediaWiki Action API. See the Not copied ... categories in Category:Data item issues.

Copyright (C) 2026 uo4tie

This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.


#!/usr/bin/python3
""" 
    # SPDX-License-Identifier:  AGPL-3.0-or-later
"""
import wikibase

# wikibase.copy_to_wikibase(API,Category,language,template-parameter,for statment: p-number)

# wikibase.copy_to_wikibase("wbsetdescription","Category:Not_copied_description","DE","description",None) # description
# wikibase.copy_to_wikibase("wbcreateclaim","Category:Not_copied_image","DE","image","P28") # image
# wikibase.copy_to_wikibase("wbcreateclaim","Category:Not copied group","DE","group","P25") # group

# wikibase.copy_to_wikibase("wbcreateclaim","Category:Not copied onNode","DE","onNode","P33") # onNode
# wikibase.copy_to_wikibase("wbcreateclaim","Category:Not copied onWay","DE","onWay","P34") # onWay
# wikibase.copy_to_wikibase("wbcreateclaim","Category:Not copied onArea","DE","onArea","P35") # onArea
# wikibase.copy_to_wikibase("wbcreateclaim","Category:Not copied onRelation","DE","onRelation","P36") # onRelation

# wikibase.copy_to_wikibase("wbcreateclaim","Category:Not copied osmcarto-rendering","DE","osmcarto-rendering","P39") # osmcarto-rendering

# wikibase.copy_to_wikibase("wbcreateclaim","Category:Not copied status","DE","status","P6") # status
# wikibase.copy_to_wikibase("wbcreateclaim","Category:Not copied statuslink","DE","statuslink","P25") # statuslink
#!/usr/bin/python3
"""
    adapted from MediaWiki API Demos at https://www.mediawiki.org/wiki/API:Categorymembers under MIT License; assisted by AI
    
    # SPDX-License-Identifier:  AGPL-3.0-or-later

"""

import requests
import login
import wikitext_parser
import category
import json

def print_tee(*args):
    with open("failure.txt", "a", encoding="utf-8") as f:
        print(*args)
        print(*args, file=f)


def strip_language_prefix(page_title):
    LANGUAGES=[
        'Af:', 'Ar:', 'Ast:', 'Az:', 'Be:', 'Bg:', 'Bn:', 'Br:', 'Bs:', 'Ca:', 'Cnr:', 'Cs',
        'Da:', 'DE:', 'Diq:', 'El:', 'en:', 'Eo:', 'ES:', 'Et:', 'Eu:', 'Fa:', 'Fi:', 'FR:', 'Fy',
        'Gcf:', 'Gd:', 'Gl:', 'He:', 'Hi:', 'Hr:', 'Ht:', 'Hu:', 'Hy:', 'Ia:', 'Id:', 'Io:', 'Is',
        'IT:', 'JA:', 'Ka:', 'Ko:', 'Ku:', 'Lb:', 'Lt:', 'Lv:', 'Mk:', 'Ml:', 'Mn:', 'Mr:', 'Ms',
        'My:', 'Ne:', 'NL:', 'Nn:', 'No:', 'Oc:', 'Pa:', 'Pl:', 'Pnb:', 'Ps:', 'Pt:', 'Ro:', 'RU',
        'Sc:', 'Shn:', 'Si:', 'Sk:', 'Skr:', 'Sl:', 'Sq:', 'Sr:', 'Sr-latn:', 'Sv:', 'Ta:', 'Th',
        'Tl:', 'Tr:', 'Tzm:', 'Uk:', 'Vi:', 'Yue:', 'Zh-hans:', 'Zh-hant:'
    ]
    
    for lang in LANGUAGES:
        if page_title.startswith(lang):
            return page_title[len(lang):]
    return page_title


def get_data_item(page_title):
    
    import requests

    page_title = strip_language_prefix(page_title)

    S = requests.Session()

    URL = "https://test.wiki.openstreetmap.org/w/api.php"

    PARAMS = {
        "action": "query",
        "prop": "pageprops",
        "ppprop": "wikibase_item",
        "titles": page_title,
        "format": "json"
    }

    R = S.get(url=URL, params=PARAMS)
    DATA = R.json()


    PAGES = DATA['query']['pages']
    page = list(PAGES.values())[0]

    if page.get('pageid') == '-1':
        return None

    item = page.get('pageprops', {}).get('wikibase_item')
    
    if not item:
        return None
    
    return item
    
    

def get_description(page):

    import requests

    S = requests.Session()

    URL = "https://test.wiki.openstreetmap.org/w/api.php"

    LANGUAGES=[
        'Af:', 'Ar:', 'Ast:', 'Az:', 'Be:', 'Bg:', 'Bn:', 'Br:', 'Bs:', 'Ca:', 'Cnr:', 'Cs',
        'Da:', 'DE:', 'Diq:', 'El:', 'en:', 'Eo:', 'ES:', 'Et:', 'Eu:', 'Fa:', 'Fi:', 'FR:', 'Fy',
        'Gcf:', 'Gd:', 'Gl:', 'He:', 'Hi:', 'Hr:', 'Ht:', 'Hu:', 'Hy:', 'Ia:', 'Id:', 'Io:', 'Is',
        'IT:', 'JA:', 'Ka:', 'Ko:', 'Ku:', 'Lb:', 'Lt:', 'Lv:', 'Mk:', 'Ml:', 'Mn:', 'Mr:', 'Ms',
        'My:', 'Ne:', 'NL:', 'Nn:', 'No:', 'Oc:', 'Pa:', 'Pl:', 'Pnb:', 'Ps:', 'Pt:', 'Ro:', 'RU',
        'Sc:', 'Shn:', 'Si:', 'Sk:', 'Skr:', 'Sl:', 'Sq:', 'Sr:', 'Sr-latn:', 'Sv:', 'Ta:', 'Th',
        'Tl:', 'Tr:', 'Tzm:', 'Uk:', 'Vi:', 'Yue:', 'Zh-hans:', 'Zh-hant:'
    ]
    
    wbptlanguage = None
    title = page
    
    for prefix in LANGUAGES:
        if page.startswith(prefix):
            wbptlanguage = prefix[:-1].lower()
            title = page[len(prefix):]
            break
    
    # if no prefix is found, use en
    if wbptlanguage is None:
        wbptlanguage = "en"


    PARAMS = {
        "action": "query",
        "prop": "pageterms",
        "titles": title,
        "wbptterms": "description",
        "wbptlanguage": wbptlanguage,
        "format": "json"
    }

    R = S.get(url=URL, params=PARAMS)
    DATA = R.json()


    PAGES = DATA["query"]["pages"]
    

    if "-1" in PAGES:
        return None
    
    
    page_data = next(iter(PAGES.values()))
    
    if "terms" in page_data:
        descriptions = page_data["terms"].get("description", [])
        if descriptions:
            return descriptions[0]

    return None
    


def write_wikibase(session, entity_id, language, value, summary, bot_flag, csrftoken, action, p, copy):
    
    params = {
        "action": action,
        "id": entity_id,
        "language": language,
        "value": value,
        "summary": summary,
        "token": csrftoken,
        "format": "json"
    }
        
    if p: # if create claim
        params = {
            "action": action,
            "entity": entity_id,
            "property": p,
            "snaktype": "value",
            "value": value,
            "summary": summary,
            "token": csrftoken,
            "format": "json"
        }
        
        
        if copy == "image" or copy == "osmcarto-rendering": # if file
            if value.startswith("File:"):
                valuef = value[5:]
                params["value"] = json.dumps(valuef) # remove file:
            else:
                return copy+" is no media file"
                
        if copy == "onNode" or copy == "onWay" or copy == "onArea" or copy == "onRelation":
            if value == "yes":
                params["value"] = json.dumps({
                "entity-type":"item",
                "numeric-id":8000
                })
            elif value == "no":
                params["value"] = json.dumps({
                "entity-type":"item",
                "numeric-id":8001
                })
            else:
                return copy+" is unspecifyed"
        
        if copy == "group":
            
            groups={
                "properties": "4671",
                "restrictions": "4672",
                "highways": "4673",
                "annotations": "4674",
                "railways": "4675",
                "natural": "4676",
                "maritime": "4677",
                "man made": "4678",
                "amenities": "4679",
                "power": "4680",
                "references": "4681",
                "addresses": "4682",
                "shops": "4683",
                "names": "4684",
                "historic": "4685",
                "buildings": "4686",
                "lifecycle": "4687",
                "tourism": "4688",
                "sports": "4689",
                "waterways": "4690",
                "aeroways": "4691",
                "religion": "4692",
                "barriers": "4693",
                "places": "4694",
                "routes": "4695",
                "boundaries": "4696",
                "marine": "4697",
                "military": "4698",
                "social networks": "4699",
                "accessibility": "4700",
                "bicycles": "4701",
                "landuse": "4702",
                "cycleways": "4704",
                "public transport": "4705",
                "placement": "4707",
                "food and beverages": "4708",
                "leisure": "4709",
                "aerialways": "4710",
                "bridges": "4712",
                "crafts": "5149",
                "offices": "19544",
                "water management": "19791",
                "agriculture": "19792",
                "advertising": "21406",
                "hazards": "21472",
                "health": "21698",
                "parking": "21703",
                "marine navigation": "21865",
                "emergencies": "21935",
                "crossings": "21939",
                "animals": "21940",
                "clubs": "21941",
                "social facilities": "21942",
                "vending machines": "21943",
                "education": "21945",
                "diplomacy": "21953",
                "ferries": "22093",
                "hiking": "22096",
                "waste processing": "22141",
                "water": "22143",
                "pipelines": "22250",
                "infrastructure": "22257",
                "weather": "22993",
                "climbing": "23061",
                "children": "23366",
                "suitable for children": "23453",
                "not suitable for children": "23454",
                "for adults only": "23456",
                "coffee": "23550",
                "sustainability": "23565",
                "circular economy": "23566",
                "top-level": "23586",
                "metro": "23614",
                "communications": "23726",
                "meals": "23751",
                "point of interest": "23782",
                "subkey": "23785",
                "vague": "23800",
                "payment methods": "23910",
                "currencies": "23921",
                "drinks": "24003"
            }
        
            params["value"] = json.dumps({
                "entity-type":"item",
                "numeric-id": groups.get(value)
            })
            
        if copy == "status":
            status={
                "approved": "15",
                "voting": "17",
                "proposed": "20",
                "de facto": "13",
                "in use": "14",
                "deprecated": "5061",
                "discardable": "7550",
                "imported": "21146",
                "obsolete": "5060",
                "undefined": "22135"
            }
            
            params["value"] = json.dumps({
                "entity-type":"item",
                "numeric-id": status.get(value)
            })
            
    if bot_flag:
        params["bot"] = "true"
        

    WIKI_URL = "https://test.wiki.openstreetmap.org/w/api.php"
    response = session.post(url=WIKI_URL, data=params)
    data = response.json()
    
    return data


def copy_to_wikibase(action,cat,lang,copy,p):
        
        # get login
        S, csrf_token = login.login()
        print("-----------------")
        
        # get pages
        pages = category.get_category_members_for_language(cat,lang)
        
        
        for page in pages:
            
            ENTITY_ID = get_data_item(page)
            if ENTITY_ID == None:
                print_tee("❌ Failure.",page+":","Entitiy ID is empty:",ENTITY_ID)
                print("-----------------")
                continue
            
            LANGUAGE = lang.lower()
            
            
            if page.startswith(str(lang+":Key:")) or page.startswith("Key:"):
                if page.endswith(":*"):
                    VALUE = wikitext_parser.get_param_for_page(page, "KeyPrefixDescription", copy)
                else:
                    VALUE = wikitext_parser.get_param_for_page(page, "KeyDescription", copy)
            elif page.startswith(str(lang+":Tag:")) or page.startswith("Tag:"):
                VALUE = wikitext_parser.get_param_for_page(page, "ValueDescription", copy)
            else:
                print_tee("❌ Failure.",page+"do not start with",str(lang+":Key:"),"or",str(lang+":Tag:"))
                print("-----------------")
                continue
                
            if VALUE == None:
                print_tee("❌ Failure.",page+":","Couldn't get the",copy,"parameter:",VALUE)
                print("-----------------")
                continue
            
            if "{{" in VALUE:
                print_tee("❌ Failure.",page+":","In",copy," is a template: ",VALUE)
                print("-----------------")
                continue
            
            SUMMARY = "Bot: Test: Updating "+copy+" from wiki page [["+page+"]]"
            BOT_FLAG = True
            
            # write
            
            print(f"write {copy} for {page } to {ENTITY_ID} ({LANGUAGE})...")
            
            result = write_wikibase(
                S, 
                ENTITY_ID, 
                LANGUAGE, 
                VALUE, 
                SUMMARY, 
                BOT_FLAG, 
                csrf_token,
                action,
                p,
                copy
            )
            
            
            # print result
            if "nochange" in result:
                print_tee("⚠️ No change!")
                print_tee(result)
            elif "success" in result:
                print("✅ Sucess!")
                print(f"Result: {result}")
            else:
                print_tee("❌ Failure:")
                if "Anti-Missbrauchs-Maßnahme" in str(result):
                    print_tee("Limit!")
                    print_tee(result)
                    break
                print_tee(result)

            print("-----------------")
#! /usr/bin/python3
"""
    adapted from MediaWiki API Demos at https://www.mediawiki.org/wiki/API:Categorymembers under MIT License; assisted by AI
    
    # SPDX-License-Identifier:  AGPL-3.0-or-later

"""

def extract_param(data, template_name, param_name):
    text = data.get("parse", {}).get("wikitext", {}).get("*", "")

    # --- 1. Die gesuchte Vorlage inkl. Verschachtelung isolieren ---
    start = text.find("{{" + template_name)
    if start == -1:
        return None

    depth, i = 1, start + 2
    while i < len(text) and depth:
        if text.startswith("{{", i):
            depth += 1; i += 2
        elif text.startswith("}}", i):
            depth -= 1; i += 2
        else:
            i += 1
    body = text[start + 2 : i - 2]   # Inhalt zwischen {{ ... }}

    # --- 2. Body an Top-Level-Pipes '|' trennen (ignoriert | in {{...}}) ---
    parts, cur, depth, eq = [], "", 0, -1
    j = 0
    while j < len(body):
        if body.startswith("{{", j):
            depth += 1; cur += "{{"; j += 2
        elif body.startswith("}}", j):
            depth -= 1; cur += "}}"; j += 2
        else:
            ch = body[j]; j += 1
            if depth == 0 and ch == "|":
                parts.append((cur, eq)); cur, eq = "", -1
            else:
                if depth == 0 and ch == "=" and eq == -1:
                    eq = len(cur)      # erstes '=' auf oberster Ebene merken
                cur += ch
    parts.append((cur, eq))

    # --- 3. Gesuchten Parameter heraussuchen ---
    for value, eq in parts:
        if eq > -1 and value[:eq].strip().lower() == param_name.strip().lower():
            return value[eq + 1:].strip()
    return None
    
    
def get_param_for_page(page_title,template,param):
    
    import requests

    S = requests.Session()

    URL = "https://test.wiki.openstreetmap.org/w/api.php"
    
    PARAMS = {
        "action": "parse",
        "page": page_title,
        "prop": "wikitext",
        "format": "json"
    }

    R = S.get(url=URL, params=PARAMS)
    DATA = R.json()

    return extract_param(DATA, template, param)
#!/usr/bin/python3
"""
    adapted from MediaWiki API Demos at https://www.mediawiki.org/wiki/API:Categorymembers under MIT License; assisted by AI
    
    # SPDX-License-Identifier:  AGPL-3.0-or-later

"""

def get_category_members_for_language(category, language: str) -> list:
    
    import requests

    S = requests.Session()

    URL = "https://test.wiki.openstreetmap.org/w/api.php"

    # Languages with their sortkey prefixes (in alphabetical order)
    LANGUAGES=['Af', 'Ar', 'Ast', 'Az', 'Be', 'Bg', 'Bn', 'Br', 'Bs', 'Ca',
    'Cnr', 'Cs', 'Da', 'DE', 'Diq', 'El', 'en', 'Eo', 'ES', 'Et', 'Eu', 'Fa',
    'Fi', 'FR', 'Fy', 'Gcf', 'Gd', 'Gl', 'He', 'Hi', 'Hr', 'Ht', 'Hu', 'Hy',
    'Ia', 'Id', 'Io', 'Is', 'IT', 'JA', 'Ka', "Key:", 'Ko', 'Ku', 'Lb', 'Lt',
    'Lv', 'Mk', 'Ml', 'Mn', 'Mr', 'Ms', 'My', 'Ne', 'NL', 'Nn', 'No', 'Oc',
    'Pa', 'Pl', 'Pnb', 'Ps', 'Pt', 'Ro', 'RU','Sc', 'Shn', 'Si', 'Sk', 'Skr',
    'Sl', 'Sq', 'Sr', 'Sr-latn', 'Sv', 'Ta', "Tag:", 'Th','Tl', 'Tr', 'Tzm',
    'Uk', 'Vi', 'Yue', 'Zh-hans', 'Zh-hant']


    # test if language is in list
    try:
        index = LANGUAGES.index(language)
    except ValueError:
        print(f"get_category_members_for_language: Language '{language}' not found")
        return []
    
    start_prefix = language
    end_prefix = LANGUAGES[index + 1] if index + 1 < len(LANGUAGES) else None
    
    PARAMS = {
        "action": "query",
        "cmtitle": category,
        "cmlimit": "2",
        "list": "categorymembers",
        "format": "json",
        "cmsort": "sortkey",
        "cmdir": "ascending",
        "cmstartsortkeyprefix": start_prefix
    }
    
    if end_prefix:
        PARAMS["cmendsortkeyprefix"] = end_prefix

    R = S.get(url=URL, params=PARAMS)
    DATA = R.json()

    PAGES = DATA.get("query", {}).get("categorymembers", [])
    
    # Filter pages that actually start with the language code
    filtered_titles = [p['title'] for p in PAGES if p.get('title', '').startswith(start_prefix)]
    
    return filtered_titles
#!/usr/bin/python3
"""
    adapted from MediaWiki API Demos at https://www.mediawiki.org/wiki/API:Categorymembers under MIT License; assisted by AI
    
    # SPDX-License-Identifier:  AGPL-3.0-or-later

"""

import requests
import sys

# Configuration - Set these values or pass as arguments
BOT_USERNAME = ""  # Format: MainAccountName@BotName
BOT_PASSWORD = ""  # Generated via Special:BotPasswords
WIKI_URL = "https://test.wiki.openstreetmap.org/w/api.php"


def get_login_token(session):
    
    #1. get login token

    params = {
        "action": "query",
        "meta": "tokens",
        "type": "login",
        "format": "json"
    }
    
    response = session.get(url=WIKI_URL, params=params)
    data = response.json()
    
    if "query" in data and "tokens" in data["query"]:
        return data["query"]["tokens"]["logintoken"]
    else:
        raise Exception(f"Fehler beim Abrufen des Login-Tokens: {data}")


def login_with_bot_password(session, username, password, login_token):
    
    #2. login with bot passwort

    params = {
        "action": "login",
        "lgname": username,
        "lgpassword": password,
        "lgtoken": login_token,
        "format": "json"
    }
    
    response = session.post(url=WIKI_URL, data=params)
    data = response.json()
    
    if data.get("login", {}).get("result") == "Success":
        print("Erfolgreich eingeloggt")
        return True
    elif data.get("login", {}).get("result") == "NeedToken":
        # Zweiter Schritt bei Some Wikis
        login_token = data["login"]["token"]
        params["lgtoken"] = login_token
        response = session.post(url=WIKI_URL, data=params)
        data = response.json()
        
        if data.get("login", {}).get("result") == "Success":
            print("Erfolgreich eingeloggt (2-Schritte)")
            return True
        else:
            raise Exception(f"Login fehlgeschlagen: {data}")
    else:
        raise Exception(f"Login fehlgeschlagen: {data}")


def get_csrf_token(session):
    
    #3. get CSRF token
    
    params = {
        "action": "query",
        "meta": "tokens",
        "format": "json"
    }
    
    response = session.get(url=WIKI_URL, params=params)
    data = response.json()
    
    if "query" in data and "tokens" in data["query"]:
        return data["query"]["tokens"]["csrftoken"]
    else:
        raise Exception(f"Fehler beim Abrufen des CSRF-Tokens: {data}")

def login():

    # create session
    S = requests.Session()
    
    try:
        #1. get login token
        print("get login token ...")
        login_token = get_login_token(S)
        
        #2. login with bot passwort
        print("logging in ...")
        login_with_bot_password(S, BOT_USERNAME, BOT_PASSWORD, login_token)
        
        #3. get CSRF-Token
        print("getting SRF token ...")
        csrf_token = get_csrf_token(S)
        
    except Exception as e:
        print(f"❌ Failure: {e}")
        sys.exit(1)
        
    return S, csrf_token