Из DOS в Федиверс: как я выпарсила 1980 цитат и пустила их по кругу в Mastodon :-)))

Дисклеймер: Писать эту статью на английском, скорее всего, нет абсолютно никакого смысла. Весь этот олдскульный фольклор с old-dos.ru, локальные шутки про суровых сисопов, падающий «маздай», диалап и коннект на 2400 бод — это чистый, непереводимый культурный код русскоязычного сегмента сети из девяностых и нулевых, который иной раз я сама не догоняю, например, "У меня мышь в коме" для меня как шутка была не очень понятна, но, как оказалось, там имелся в виду COM-порт. Поэтому текст остаётся на великом и могучем (а местами — на KOI8-R).


Всё началось с того, что я зашла на сайт old-dos.ru и залипла на цитаты в нижней части страницы. Фидошный сленг, шутки про падающий маздай и оборванный коннект — всё это было слишком хорошо, чтобы просто оставить это пылиться на старом сайте. Я решила утащить их к себе.


Сначала я пошла по простому пути: попыталась выкачать сайт целиком, надеясь найти заветный текстовый файл со всеми шутками внутри. План провалился — файла в открытом виде не оказалось или я искала плохо, а может быть смотрела не туда.


Пришлось расчехлять Rust. Я набросала небольшой скрипт-парсер, который делал простую вещь: обновлял страницу, выдирал цитату, проверял, есть ли она уже в моей базе, и если нет — дописывал в текстовый файл. Скрипт покрутился какое-то время и собрал мне шикарную базу — ровно 1980 оригинальных цитат.


Дальше эту сырую массу нужно было подготовить для бота. Я набросала быстрый shell-скрипт, который на лету вычистил мусорные теги, заэкранировал кавычки и аккуратно перегнал весь текст в плоский JSON вида { "id": 1, "text": "..." }.


Имея на руках готовую базу, я написала второго бота на Rust — уже для Mastodon. Логика простая: бот берёт случайную цитату, проверяет по локальному файлу, чтобы не постить баяны в текущем цикле, и отправляет в ленту. Чтобы всё это работало на моей Raspberry Pi Zero, бинарник был скомпилирован под ARMv6 и повешен в крон на публикацию три раза в день. 1980 цитат хватит почти на два года без повторов.


На этом можно было бы закончить, но душа требовала эстетики. Раз уж у меня есть такой архив, я решила сделать из него базу для классических UNIX-утилит fortune и cowsay. Пара команд в консоли, немного магии с перегоном UTF-8 в KOI8-R (чтобы облачко не разваливалось из-за русских букв) — и теперь при каждом запуске терминала меня встречает ASCII-коала, выдающая рандомную олдскульную шутку.


Акт I. Как выкачать наследие, или Парсинг с привкусом Windows-1251


Прежде чем форматировать JSON и настраивать кросс-компиляцию для Mastodon-бота, нужно было собрать саму базу. Сайт old-dos.ru — это настоящий портал в прошлое. Никаких удобных API или готовых архивов для скачивания там, конечно же, нет. Цитата генерируется случайным образом при каждом обновлении главной страницы и висит где-то в самом низу.


Раз готового файла нет, значит, сделаем свой. Задача для скрипта вырисовывалась предельно четкая: бесконечно обновлять страницу, находить блок с текстом, проверять, есть ли такая шутка в моей коллекции, и если нет — аккуратно дописывать в файл.


Для этой задачи я набросала отдельный мини-проект на Rust. В Cargo.toml понадобилось добавить всего три зависимости: reqwest для самих HTTP-запросов, scraper для ковыряния HTML-дерева и encoding_rs. Последняя библиотека оказалась критически важной. Старые сайты суровы и до сих пор живут в кодировке Windows-1251, от которой современные парсеры, по умолчанию ожидающие UTF-8, просто давятся и выдают нечитаемые кракозябры.


Файл конфигурации получился таким:


[package]
name = "olddos"
version = "0.1.0"
edition = "2021"

[dependencies]
reqwest = { version = "0.11", features = ["blocking"] }
scraper = "0.18"
encoding_rs = "0.8"

А вот и сам код скрипта-добытчика:


use reqwest::blocking::Client;
use scraper::{Html, Selector};
use std::collections::HashSet;
use std::fs::OpenOptions;
use std::io::Write;
use std::thread;
use std::time::Duration;

fn main() {
    let url = "http://old-dos.ru/";
    let mut unique_quotes = HashSet::new();
    let file_name = "old_dos_quotes.txt";

    let client = Client::builder()
        .user_agent("Mozilla/5.0 (X11; Fedora; Linux x86_64) AppleWebKit/537.36")
        .build()
        .unwrap();

    println!("📡 Начинаю бесконечный сбор приколов.");

    let mut total_attempts = 0;

    loop {
        total_attempts += 1;

        if let Ok(response) = client.get(url).send() {
            if let Ok(bytes) = response.bytes() {
                let (decoded, _, _) = encoding_rs::WINDOWS_1251.decode(&bytes);
                let html = Html::parse_document(&decoded);
                let selector = Selector::parse("body").unwrap();

                if let Some(body) = html.select(&selector).next() {
                    let lines: Vec<String> = body.text()
                        .map(|t| t.trim().to_string())
                        .filter(|t| !t.is_empty())
                        .collect();

                    if let Some(pos) = lines.iter().position(|l| l.contains("For abuses")) {
                        if pos > 0 {
                            let quote = lines[pos - 1].clone();

                            if quote.len() > 5 && !quote.contains("Поиск") && !quote.contains("1 2 3") {
                                if unique_quotes.insert(quote.clone()) {
                                    println!("[Новая! #{}] {}", unique_quotes.len(), quote);

                                    let mut file = OpenOptions::new()
                                        .create(true)
                                        .append(true)
                                        .open(file_name)
                                        .unwrap();

                                    if let Err(e) = writeln!(file, "{}", quote) {
                                        eprintln!("Ошибка записи: {}", e);
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }

        print!("\rПопытка: {} | Уникальных: {} ", total_attempts, unique_quotes.len());
        std::io::stdout().flush().unwrap();

        thread::sleep(Duration::from_millis(500));
    }
}

Что здесь происходит под капотом:


Я запустила этот код и пошла заниматься своими делами. Статус в консоли компактно обновлялся в одну строчку через символ возврата каретки \r, показывая количество попыток и пополнение словаря. Когда счетчик перевалил за 1980 уникальных цитат, а новые стали появляться совсем уж редко, я просто нажала кнопку 'Stop' в RustRover.


Доверять коду на 100% — не мой метод. Поэтому перед тем, как скармливать готовый old_dos_quotes.txt питоновскому скрипту для генерации JSON, я сделала финальную проверку на чистоту прямо в терминале с помощью старой доброй классики:


sort old_dos_quotes.txt | uniq -d

Эта команда прогоняет текст через пайп, сортирует его и выводит на экран только те строки, которые повторяются больше одного раза. В итоге 1980 собранных цитат были абсолютно уникальными и базу можно было смело отправлять на форматирование.


Сырая база была готова. Дальше её ждало превращение в стройный JSON для Mastodon-бота.


Акт II. Укрощение спецсимволов, или Нафига мне сдался JSON


Сырая база из почти двух тысяч цитат лежала в текстовом файле, и на первый взгляд казалось, что её можно просто скармливать боту построчно. Возникает резонный вопрос: нафига вообще было городить огород с конвертацией в JSON?


Причин было две, и обе критические:


JSON решает обе проблемы изящно и раз и навсегда.


Для этой трансформации я написала компактный скрипт на Python:


import json
import re

with open("old_dos_quotes.txt", "r", encoding="utf-8") as f:
    content = f.read()

content = re.sub(r'\\s*', '', content)
lines = [line.strip() for line in content.split('\n') if line.strip()]

with open("quotes_formatted.txt", "w", encoding="utf-8") as f:
    for i, line in enumerate(lines, 1):
        text_json = json.dumps(line, ensure_ascii=False)
        f.write(f'  {{ "id": {i}, "text": {text_json} }},\n')

print("Фсё кончено! Файлик готовченко!")

Как работает эта магия:


Скрипт выплюнул просто список объектов через запятую. Чтобы строгий парсер в Rust не сошел с ума и смог прочитать это как массив, нужно открыть получившийся файл и ручками поставить квадратную скобку `[` в самой первой строке и `]` в самой последней (заодно убрав висячую запятую у последней цитаты).


Должно получится как-то так:

[
  { "id": 1, "text": "Настоящие программисты не пишут на Паскале..." },
  { "id": 2, "text": "A bad day: transfer completed (5720468 bytes, 1 CPS)" },
...
  { "id": 1980, "text": "У меня мышь в коме" }
]

Акт III. Бот, который не повторяется: пишем логику на Rust


Когда данные собраны и аккуратно упакованы в quotes.json, остаётся самое главное — заставить всё это работать автономно. Логика бота должна быть надёжной: выбирать случайную цитату, не допускать баянов в пределах одного цикла (а это, на минуточку, почти два года), прикреплять картинки, если они есть, и уметь удалять свои же старые посты, чтобы не превращать сервер в свалку дубликатов.


Проект собирался с минимальным набором зависимостей. Для работы с сетью используется reqwest с легковесным rustls — это критично для успешной кросс-компиляции под слабую архитектуру ARMv6 без головной боли с библиотеками C.


Файл конфигурации Cargo.toml выглядит максимально аскетично:


[package]
name = "olddos-quote-bot"
version = "0.1.0"
edition = "2021"

[dependencies]
reqwest = { version = "0.11", features = ["json", "blocking", "rustls-tls", "multipart"], default-features = false }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
dotenv = "0.15"
rand = "0.8"
anyhow = "1.0"

Секретные материалы: прячем ключи от Mastodon


Чтобы бот мог отправлять посты, ему нужен доступ к нашему аккаунту в Mastodon. Вшивать токены прямо в исходный код на Rust — плохая практика.


Для этого в проекте используется библиотека dotenv, которая умеет подтягивать секреты из локального файла. Всё, что нужно сделать на сервере (в нашем случае на Raspberry Pi), — это создать файл с именем .env ровно в той же директории, из которой запускается сам бинарник скрипта.


Например, для моей структуры папок я просто создаю файл по пути /home/rachel/Mastodon/rust/olddos/.env и прописываю в нём две строки:


MASTODON_INSTANCE_URL="https://mastodon.social"
MASTODON_ACCESS_TOKEN="МОЙ_ТОКЕН_ОТ_СЛОНОЗАВРА"

Сам код (src/main.rs) уместился в сотню строк. Он читает базу, сверяется со стейтом в published.json, выбирает свежую шутку, форматирует её в виде классического C-комментария и отправляет в Федиверс:


use anyhow::{Context, Result};
use rand::seq::SliceRandom;
use reqwest::blocking::{multipart, Client};
use serde::{Deserialize, Serialize};
use std::{env, fs, path::Path};

#[derive(Serialize, Deserialize, Clone, Debug)]
struct Quote {
    id: u32,
    text: String,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
struct PostRecord {
    quote_id: u32,
    last_post_id: String,
    posted_this_cycle: bool,
}

fn main() -> Result<()> {
    dotenv::dotenv().ok();

    let instance_url = env::var("MASTODON_INSTANCE_URL")
        .context("MASTODON_INSTANCE_URL missing in environment")?;
    let token = env::var("MASTODON_ACCESS_TOKEN")
        .context("MASTODON_ACCESS_TOKEN missing in environment")?;

    let quotes_path = "quotes.json";
    let state_path = "published.json";
    let images_dir = "images";

    let quotes_data = fs::read_to_string(quotes_path)
        .context("Failed to read quotes.json. Ensure the file exists.")?;
    let all_quotes: Vec<Quote> = serde_json::from_str(&quotes_data)
        .context("Failed to parse quotes.json structure.")?;

    let mut state: Vec<PostRecord> = if Path::new(state_path).exists() {
        let data = fs::read_to_string(state_path)?;
        serde_json::from_str(&data).unwrap_or_default()
    } else {
        Vec::new()
    };

    let mut available_quotes: Vec<Quote> = all_quotes
        .iter()
        .filter(|q| !state.iter().any(|r| r.quote_id == q.id && r.posted_this_cycle))
        .cloned()
        .collect();

    if available_quotes.is_empty() {
        println!("🔄 Cycle complete. Resetting 'posted_this_cycle' flags.");
        for record in &mut state {
            record.posted_this_cycle = false;
        }
        fs::write(state_path, serde_json::to_string_pretty(&state)?)?;
        available_quotes = all_quotes.clone();
    }

    let selected_quote = available_quotes
        .choose(&mut rand::thread_rng())
        .context("No available quotes found to post.")?;

    let client = Client::builder().use_rustls_tls().build()?;

    if let Some(old_record) = state.iter().find(|r| r.quote_id == selected_quote.id) {
        if !old_record.last_post_id.is_empty() {
            println!("🗑 Deleting old post version (ID: {})...", old_record.last_post_id);
            let delete_url = format!("{}/api/v1/statuses/{}", instance_url, old_record.last_post_id);
            let _ = client.delete(&delete_url).bearer_auth(&token).send();
        }
    }

    let image_path = format!("{}/{}.jpg", images_dir, selected_quote.id);
    let mut media_id = None;

    if Path::new(&image_path).exists() {
        println!("📸 Found image for quote #{}. Uploading...", selected_quote.id);
        let form = multipart::Form::new().file("file", &image_path)?;

        let media_res = client
            .post(format!("{}/api/v1/media", instance_url))
            .bearer_auth(&token)
            .multipart(form)
            .send()?;

        if let Ok(m_data) = media_res.json::<serde_json::Value>() {
            if let Some(id) = m_data["id"].as_str() {
                media_id = Some(id.to_string());
                println!("✅ Media uploaded (ID: {})", id);
            }
        }
    }

    let status_text = format!(
        "\"{}\"\n\n/* Цитата №{} с сайта old-dos.ru */",
        selected_quote.text, selected_quote.id
    );
    let mut form_data = vec![("status", status_text)];

    if let Some(ref id) = media_id {
        form_data.push(("media_ids[]", id.clone()));
    }

    let res = client
        .post(format!("{}/api/v1/statuses", instance_url))
        .bearer_auth(&token)
        .form(&form_data)
        .send()?;

    if res.status().is_success() {
        let res_data: serde_json::Value = res.json()?;
        let new_post_id = res_data["id"].as_str().unwrap_or_default().to_string();

        println!("🚀 Quote #{} successfully posted! New Post ID: {}", selected_quote.id, new_post_id);

        if let Some(record) = state.iter_mut().find(|r| r.quote_id == selected_quote.id) {
            record.last_post_id = new_post_id;
            record.posted_this_cycle = true;
        } else {
            state.push(PostRecord {
                quote_id: selected_quote.id,
                last_post_id: new_post_id,
                posted_this_cycle: true,
            });
        }

        fs::write(state_path, serde_json::to_string_pretty(&state)?)
            .context("Failed to save state to published.json")?;
    } else {
        eprintln!("❌ API Error: {} - {}", res.status(), res.text().unwrap_or_default());
    }

    Ok(())
}

Что под капотом? Разбираем логику


Код может показаться монолитным, но на деле он работает как швейцарские часы и решает сразу несколько неочевидных проблем.

Бот не просто держит в памяти 1980 цитат. Рядом с основной базой (quotes.json) лежит динамический файл состояния. В нём записывается, какие цитаты уже были опубликованы в текущем «круге», и под какими ID эти посты лежат в Mastodon. При каждом запуске скрипт считывает оба файла и отсеивает то, что уже публиковалось.

Что произойдёт, когда бот запостит последнюю, 1980-ю шутку? Ничего не сломается. В коде есть проверка: если пул доступных цитат пуст, скрипт понимает, что цикл завершён. Он автоматически сбрасывает все флаги posted_this_cycle на false и просто начинает новый круг. При частоте публикаций 3 раза в день один такой цикл занимает почти два года.

Это моя любимая фича. Когда бот заходит на второй (или третий) круг и выбирает случайную цитату, он смотрит в свой published.json. Если он видит, что два года назад уже постил эту шутку, он берёт её старый last_post_id и отправляет в Mastodon API запрос DELETE. Так лента аккаунта всегда остаётся чистой, актуальной и не превращается в бесконечную свалку дубликатов.

У цитат могут быть графические версии или тематические мемы, которые я предполагаю накидать в папку позже. Бот перед публикацией проверяет, есть ли в папке images/ файл с именем <id_цитаты>.jpg. Если файл найден, скрипт сначала загружает его через Media API, получает media_id от сервера, и только потом прикрепляет к тексту поста.

Сам текст отправляется не просто голым куском. С помощью макроса format! бот элегантно оборачивает подпись в классический синтаксис языка Си. В ленте это выглядит так, будто кто-то оставил комментарий в исходном коде:

/* Цитата №432 с сайта old-dos.ru */


Благодаря reqwest с легковесным бэкендом rustls-tls, весь этот механизм отлично компилируется под ARMv6 через утилиту cross прямо на моей Fedora. Никакой возни с сишным OpenSSL и зависимостями — на выходе получается один статический бинарник, который я просто закинула на малинку и повесила в crontab.


Код получился монолитным и пуленепробиваемым. Оставалось только собрать его под архитектуру Raspberry Pi Zero.


Акт IV. Ржавчина на Малинке, или Кросс-компиляция без боли


Писать код — это полдела. Настоящее веселье начинается, когда этот код нужно запустить на целевом железе. Моим сервером выступает Raspberry Pi Zero первой ревизии. Это отличная, безотказная железка, но у неё скромный процессор архитектуры ARMv6 и всего 512 МБ оперативной памяти.


Собирать проект на Rust прямо на ней — затея для сильных духом. Компиляция одного только крейта reqwest со всеми зависимостями заняла бы вечность, а скорее всего, процесс просто был бы убит системой из-за нехватки ОЗУ (OOM Killer). Значит, собирать бинарник нужно на основном ПК с Fedora, а на Raspberry отправлять уже готовый исполняемый файл.


Тут возникает классическая проблема: моя Fedora работает на десктопной архитектуре x86_64, а малинка — это ARMv6. Если просто сделать дефолтный cargo build --release, сервер этот файл даже не прочитает. Нужна кросс-компиляция.


Раньше настройка тулчейна для кросс-компиляции в Linux напоминала пляски с бубном: нужно было качать правильные версии gcc, прописывать пути, бороться с несовместимостью версий системной библиотеки glibc... Но в экосистеме Rust есть потрясающая утилита cross. Она работает поверх контейнеров (Podman или Docker) и берет всю грязную работу по подтягиванию нужных зависимостей на себя.


Ставится она элементарно:


cargo install cross

Дальше нужно было выбрать правильный таргет (целевую платформу). Для старых Raspberry Pi идеальным вариантом является сборка со статической линковкой библиотеки musl. Это монолитный подход: все нужные зависимости вшиваются прямо в бинарник, что гарантирует его запуск на сервере вообще без оглядки на то, какие системные пакеты там стоят. Мой таргет назывался arm-unknown-linux-musleabihf.


Запускаем сборку:


cross build --target arm-unknown-linux-musleabihf --release

Пару минут шуршания кулерами на десктопе, утилита сама скачала нужный контейнер с окружением ARM, скомпилировала код, и в папке target появился заветный, полностью статичный исполняемый файл olddos-quote-bot.


Оставалось только перекинуть его по SSH на малинку в подготовленную папку:


scp olddos-quote-bot rachel@<ip_малинки>:/home/rachel/Mastodon/rust/olddos/

Автоматизация рутины


Чтобы бот жил своей жизнью и регулярно радовал Федиверс олдскулом, его нужно было автоматизировать. Возиться с systemd ради скрипта, который отрабатывает за две секунды и засыпает, не было смысла. Старый добрый cron тут подходит идеально.


Я зашла на малинку, открыла расписание командой crontab -e и добавила туда строчку, чтобы бот публиковал посты три раза в день.


Всё. Механизм запущен. Скрипт просыпается по расписанию, читает спрятанные в .env ключи, выбирает шутку, проверяет её на уникальность в текущем цикле и отправляет в Mastodon.


Акт IV. Эстетика терминала: как заставить коалу говорить по-русски и не ломать облачко


Бот тихо шуршал в фоне на «малинке», отправляя посты по расписанию, но мне этого показалось мало. Имея на руках почти две тысячи отборных олдскульных цитат, было бы преступлением не интегрировать их в свою повседневную рутину. Душа просила классики: я хотела, чтобы при каждом открытии консоли меня встречала случайная шутка, выведенная через утилиты fortune и cowsay. И раз уж исторически сложилось, что мой вес 53 кило на момент написания статьи, а так же принимая во внимание, что мне всё пофигу, то вещать должена была не корова, а малышка ASCII-коала.


Сначала нужно было скормить нашу текстовую базу утилите fortune. Ей не нужен JSON, но у неё есть свой строгий формат: каждая цитата должна отделяться строкой, содержащей единственный символ %.


Делать это руками на 1980 строк — безумие, поэтому в дело вступил awk. Я набросала простенький скрипт, который брал исходный файл old_dos_quotes.txt, вычищал возможный мусор и расставлял разделители:


awk '
{
    if ($0 ~ /[^ \t]/) {
        print $0
        print "%"
    }
}' old_dos_quotes.txt > olddos

На выходе получился готовый текстовый файл olddos. Но fortune не читает текст напрямую, ему нужен бинарный индекс для быстрого поиска. Создается он одной командой:


strfile olddos olddos.dat

Затем оба файла (olddos и olddos.dat) отправились в системную директорию /usr/share/games/fortune/ (пути могут немного отличаться, иногда это /usr/share/fortune/).


Казалось бы, всё готово. Я предвкушала триумф, пишу: fortune olddos | cowsay -f koala... и получаю визуальную катастрофу.


Облачко текста развалилось. Правая граница улетела куда-то далеко вправо, текст съехал.


_________________________________________
/ Целый час я кэшировал \
| клавиатуру на                |
\ запись...                    /
 -----------------------------------------
   \
    \
       ___
     {~._.~}
      ( Y )
     ()~*~()
     (_)-(_)

Разбор полётов и битва кодировок


Проблема оказалась до боли банальной, но от того не менее интересной. Утилита cowsay была написана на Perl еще в конце 90-х. Она живет в мире однобайтового ASCII и вообще ничего не знает про современный юникод.


В нашей современной UTF-8 одна русская буква кириллицы занимает 2 байта. Cowsay тупо считает байты, чтобы вычислить длину самой длинной строки и нарисовать рамку. Для неё слово «запись» состоит не из 6 символов, а из 12 байт. Она отсчитывает 12 позиций, ставит правую границу |, а сам терминал отрисовывает визуально короткое слово. Вот рамка и уезжает вправо, оставляя дыры.


Решение напрашивалось само собой: нужно было вернуть коалу в её родную эпоху. Нужна была однобайтовая кодировка, где одна русская буква честно равна одному байту. Идеальный кандидат — легендарная KOI8-R.


Я собрала вот такой конвейер (pipeline) с помощью утилиты iconv:


fortune olddos | iconv -f utf8 -t koi8-r | cowsay -f koala | iconv -f koi8-r -t utf8

Как работает эта магия:


Результат — идеально ровное облачко с кириллицей. Хак сработал безупречно.


 _________________________________
/ Если вам надоели кошки, собаки, \
\ тараканы - заводите хакера!     /
 ---------------------------------
  \
   \
       ___  
     {~._.~}
      ( Y )
     ()~*~()   
     (_)-(_)  

Чтобы не вводить эту страшную колбасу из пайпов руками, я просто прописала её как алиас в свой ~/.bashrc и добавила вызов в конец файла:


alias koala='fortune olddos | iconv -f utf8 -t koi8-r | cowsay -f koala | iconv -f koi8-r -t utf8'
koala

Теперь каждый раз, когда я открываю новую вкладку терминала или логинюсь по SSH, меня встречает невозмутимая коала с ровненьким диалоговым окном, выдающая что-то вроде: "Настоящие программисты не пишут на Паскале, потому что там нет команды PEEK и POKE".


Шалость удалась. Наследие old-dos.ru сохранено, расфасовано, автоматизировано и теперь живет сразу в двух мирах: в Федиверсе и в моем локальном терминале.


Эпилог: О цензуре, маце и наследии эпохи


Было бы нечестно сказать, что весь этот текстовый архив состоит сплошь из милых гиковских баек и безобидного фидошного сленга. Парсинг старого интернета — это как археология: иногда откапываешь то, от чего хочется вымыть руки с мылом.


Прогоняя базу через скрипты, я наткнулась на такой перл: "Комсомольцу не к лицу: пить, курить и есть мацу...".


Признаюсь, в первую секунду рука сама потянулась написать скрипт, чтобы поставить old-dos.ru на уютный DDoS, а автора этого шедевра пустить по кругу точно так же, как мой бот пускает эти цитаты в Mastodon. Но, немного остыв и поразмыслив, я пришла к логичному выводу: а ведь и правда, еврею быть комсомольцем — как-то стыдно.


В итоге я решила не играть в цензора и оставила эту цитату в обойме. В конце концов, из песни слов не выкинешь, это тоже слепок времени и наследие той эпохи. Да и кто знает — может, кому-то в ленте она попадётся на глаза и будет очень приятна как тёплое семейное воспоминание о том, что именно его дед на полставки охранял Освенцим и на вторые пол ставки ГУЛАГ.


---

Перейти в секцию Raspberry Pi & Hardware

Назад на Главную страницу



/raspberry/