diff --git a/.editorconfig b/.editorconfig index 4e8e071..6e5b9f9 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,11 +9,6 @@ charset = utf-8 end_of_line = lf insert_final_newline = true -[*.py] -indent_style = space -indent_size = 4 - -[*.yaml] +[*.{js,yaml}] indent_style = space indent_size = 2 - diff --git a/.forgejo/workflows/deploy.yaml b/.forgejo/workflows/deploy.yaml index 1b6cc42..04e8003 100644 --- a/.forgejo/workflows/deploy.yaml +++ b/.forgejo/workflows/deploy.yaml @@ -1,8 +1,9 @@ name: deploy blog + on: workflow_dispatch: schedule: - - cron: '0 10 * * 3' # at 10:00 on wednesday + - cron: '0 10 * * *' # daily at 10:00 push: branches: - staging @@ -10,46 +11,44 @@ on: jobs: deploy: - runs-on: ubuntu-latest - steps: + runs-on: alpine-latest + steps: - name: Install dependencies - run: apk --no-cache add hugo python3 py3-pip git openssh-client rsync + run: apk --no-cache add hugo git openssh-client rsync - name: Check versions run: | cat /etc/os-release git version hugo version - python --version - + rsync --version + ssh -V + + - name: Set envionment vars + run: echo "GIT_BRANCH=${{ forgejo.event_name == 'schedule' && 'production' || forgejo.ref_name }}" >> "$FORGEJO_ENV" + - name: Checkout repository run: | - git clone -b ${{ forgejo.ref_name }} --recursive https://git.berlin.ccc.de/cccb-website-team/www.git . + git clone -b $GIT_BRANCH --recursive https://git.berlin.ccc.de/cccb-website-team/www.git . git status - - name: Install Python depenndencies - run: python -m pip install -r requirements.txt --break-system-packages - - name: Render site run: ./build.sh - name: Setup SSH - env: - SSH_PRIVATE_KEY: ${{ forgejo.ref_name == 'production' && secrets.SSH_PRIVATE_KEY_PRODUCTION || secrets.SSH_PRIVATE_KEY_STAGING }} run: | mkdir -p ~/.ssh printf "%s" "${{ secrets.KNOWN_HOSTS }}" | base64 -d > ~/.ssh/known_hosts - printf "%s" "$SSH_PRIVATE_KEY" | base64 -d > ~/.ssh/id_ed25519 + printf "%s" "${{ env.GIT_BRANCH == 'production' && secrets.SSH_PRIVATE_KEY_PRODUCTION || secrets.SSH_PRIVATE_KEY_STAGING }}" | base64 -d > ~/.ssh/id_ed25519 chmod 600 ~/.ssh/id_ed25519 ssh-keygen -f ~/.ssh/id_ed25519 -y > ~/.ssh/id_ed25519.pub cat ~/.ssh/id_ed25519.pub - name: Rsync rendered site - env: - DEPLOY_DIR: ${{ forgejo.ref_name == 'production' && '/srv/http/www/' || '/srv/http/www-staging/' }} # TODO: add --delete - run: rsync -var -e 'ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes' ./public/ deploy@www.berlin.ccc.de:$DEPLOY_DIR + run: rsync -var -e 'ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=yes' ./public/ deploy@www.berlin.ccc.de:${{ env.GIT_BRANCH == 'production' && '/srv/http/www/' || '/srv/http/www-staging/' }} - name: Cleanup if: ${{ always() }} run: rm -rf ~/.ssh + diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index ae018fa..0000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -*.ics text eol=crlf diff --git a/.gitignore b/.gitignore index 18286ce..008e122 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,9 @@ -static/all.ics +# Created by https://www.toptal.com/developers/gitignore/api/windows,linux,macos,hugo,pycharm+all,vim,direnv +# Edit at https://www.toptal.com/developers/gitignore?templates=windows,linux,macos,hugo,pycharm+all,vim,direnv + +### direnv ### +.direnv .envrc -*.swp -# Created by https://www.toptal.com/developers/gitignore/api/windows,linux,macos,hugo -# Edit at https://www.toptal.com/developers/gitignore?templates=windows,linux,macos,hugo ### Hugo ### # Generated files by hugo @@ -41,8 +42,7 @@ hugo.linux .LSOverride # Icon must end with two \r -Icon - +Icon # Thumbnails ._* @@ -67,6 +67,114 @@ Temporary Items # iCloud generated files *.icloud +### PyCharm+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### PyCharm+all Patch ### +# Ignore everything but code style settings and run configurations +# that are supposed to be shared within teams. + +.idea/* + +!.idea/codeStyles +!.idea/runConfigurations + +### Vim ### +# Swap +[._]*.s[a-v][a-z] +!*.svg # comment out if you don't need vector files +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ + ### Windows ### # Windows thumbnail cache files Thumbs.db @@ -93,17 +201,4 @@ $RECYCLE.BIN/ # Windows shortcuts *.lnk -# End of https://www.toptal.com/developers/gitignore/api/windows,linux,macos,hugo - -# NixOS: -.envrc -shell.nix -.direnv - -# Python -.venv - -# murmeldin -overview.md - -/result +# End of https://www.toptal.com/developers/gitignore/api/windows,linux,macos,hugo,pycharm+all,vim,direnv diff --git a/.python-version b/.python-version deleted file mode 100644 index 2c07333..0000000 --- a/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.11 diff --git a/README.md b/README.md index 86698bc..5f72bd0 100644 --- a/README.md +++ b/README.md @@ -27,10 +27,20 @@ This is the website of the CCCB. 5. Point your browser to: -To ready your site for upload, run `./build.sh`, which also generates `all.ics` -and adds the calendar table to `index.html`. -Every change you make on the project will be reflected in your browser -as long as `hugo serve` is running. +Every change you make on the project will be reflected in your browser as long as `hugo serve` is running. + +The *"Nächste Veranstaltungen"* table on the home page is generated by post-processing in `./build.sh`, not by Hugo, so +it is **not** visible under `hugo serve`. To preview the fully built site (including the home-page calendar), or to +ready the site for upload, run: + +```shell +./build.sh +python3 -m http.server -d public 1313 +``` + +`build.sh` replaces the `CALENDAR` placeholder in `index.html` with the upcoming-events table. It depends on Python +with the `icalendar`, `python-dateutil`, and `pytz` packages, plus a `de_DE.UTF-8` locale (used to format weekday +names). Inside `nix develop` these are provided automatically. To build with *nix*: `nix build '.?submodules=1#production-content'` @@ -41,7 +51,8 @@ To build with *nix*: `nix build '.?submodules=1#production-content'` 3. Commit (and push) your change. 4. ~~GitHub Actions is running the release workflow.~~ - If successful, check [Staging Website](https://staging.berlin.ccc.de/) if change is correct. -5. Create merge request to merge changes from `staging` to `production` branch. Ask somebody to check merge request or if small change, merge yourself. +5. Create a merge request to merge changes from `staging` to `production` branch. Ask somebody to check merge request or + if small change, merge yourself. 6. ~~GitHub Actions is running the release workflow.~~ - If successfull, check [Website](https://berlin.ccc.de/) if change is correct. 7. Profit! @@ -50,9 +61,12 @@ To build with *nix*: `nix build '.?submodules=1#production-content'` - After entering the shell with `nix develop`, hugo is available and `hugo serve` should work - Python including required packages will be available, so the `build.sh` should work without a venv -- You can build the staging and production builds with `nix build .#staging-content` and `nix build .#production-content` -- Do not update the nixpkgs branch - 25.05 contains a newer hugo version that is incompatible with the theme (last checked June 2025) +- You can build the staging and production builds with `nix build .#staging-content` and + `nix build .#production-content` +- Do not update the nixpkgs branch - 25.05 contains a newer hugo version that is incompatible with the theme (last + checked June 2025) --- Made with ❤️ and [Hugo](https://gohugo.io). + diff --git a/assets/js/calendar.js b/assets/js/calendar.js index 5aa9d6b..18b4768 100644 --- a/assets/js/calendar.js +++ b/assets/js/calendar.js @@ -19,10 +19,10 @@ document.addEventListener('DOMContentLoaded', function() { if (colonIndex > -1) { let key = line.substring(0, colonIndex); let value = line.substring(colonIndex + 1); - + // Handle properties with parameters (like TZID) const baseKey = key.split(";")[0]; - + if (baseKey === "DTSTART") { event.start = value; event.startParams = key.includes(";") ? key.substring(key.indexOf(";") + 1) : null; @@ -46,14 +46,14 @@ document.addEventListener('DOMContentLoaded', function() { function parseDateString(icsDateStr) { // Handle different date formats if (!icsDateStr) return null; - + // For basic date format: YYYYMMDD if (icsDateStr.length === 8) { let year = icsDateStr.substring(0, 4); let month = icsDateStr.substring(4, 6); let day = icsDateStr.substring(6, 8); return `${year}-${month}-${day}`; - } + } // For datetime formats: YYYYMMDDTHHmmssZ or YYYYMMDDTHHmmss else if (icsDateStr.includes("T")) { let year = icsDateStr.substring(0, 4); @@ -67,34 +67,34 @@ document.addEventListener('DOMContentLoaded', function() { // Extract date components from different date formats function getDateComponents(icsDateStr) { if (!icsDateStr) return null; - + // Basic handling - extract YYYY, MM, DD regardless of format const year = parseInt(icsDateStr.substring(0, 4)); const month = parseInt(icsDateStr.substring(4, 6)) - 1; // 0-based months const day = parseInt(icsDateStr.substring(6, 8)); - + return { year, month, day }; } function expandRecurringEvents(event, year, month) { if (!event.rrule) return [event]; - + const rruleStr = event.rrule; - + // Get start date components const startComponents = getDateComponents(event.start); if (!startComponents) return [event]; - + const startDate = new Date( startComponents.year, startComponents.month, startComponents.day ); - + const rangeStart = new Date(year, month, 1); const rangeEnd = new Date(year, month + 1, 0); const expandedEvents = []; - + if (rruleStr.includes("FREQ=WEEKLY") && rruleStr.includes("BYDAY")) { const bydayMatch = rruleStr.match(/BYDAY=([^;]+)/); if (bydayMatch) { @@ -103,7 +103,7 @@ document.addEventListener('DOMContentLoaded', function() { 'MO': 1, 'TU': 2, 'WE': 3, 'TH': 4, 'FR': 5, 'SA': 6, 'SU': 0 }; const targetDay = dayMap[dayCode]; - + if (targetDay !== undefined) { // Create events for each matching day in the month let day = 1; @@ -112,13 +112,13 @@ document.addEventListener('DOMContentLoaded', function() { if (testDate.getDay() === targetDay && testDate >= startDate) { const newEvent = {...event}; const eventDate = formatDateForICS(testDate); - + // Preserve time portion from original event - const timePart = event.start.includes('T') ? + const timePart = event.start.includes('T') ? event.start.substring(event.start.indexOf('T')) : ''; - const endTimePart = event.end.includes('T') ? + const endTimePart = event.end.includes('T') ? event.end.substring(event.end.indexOf('T')) : ''; - + newEvent.start = eventDate + timePart; newEvent.end = eventDate + endTimePart; expandedEvents.push(newEvent); @@ -131,19 +131,25 @@ document.addEventListener('DOMContentLoaded', function() { else if (rruleStr.includes("FREQ=MONTHLY") && rruleStr.includes("BYDAY")) { const bydayMatch = rruleStr.match(/BYDAY=([^;]+)/); if (bydayMatch) { + const intervalMatch = rruleStr.match(/INTERVAL=(\d+)/); + const interval = intervalMatch ? parseInt(intervalMatch[1]) : 1; + const monthsFromStart = (year - startDate.getFullYear()) * 12 + (month - startDate.getMonth()); + if (monthsFromStart < 0 || monthsFromStart % interval !== 0) { + return []; + } const bydays = bydayMatch[1].split(','); const dayMap = { 'MO': 1, 'TU': 2, 'WE': 3, 'TH': 4, 'FR': 5, 'SA': 6, 'SU': 0 }; - + bydays.forEach(byday => { const occurrence = parseInt(byday) || 1; const dayCode = byday.slice(-2); const dayIndex = dayMap[dayCode]; - + let day = 1; let count = 0; - + while (day <= rangeEnd.getDate()) { const testDate = new Date(year, month, day); if (testDate.getDay() === dayIndex) { @@ -151,13 +157,13 @@ document.addEventListener('DOMContentLoaded', function() { if (count === occurrence || (occurrence < 0 && day > rangeEnd.getDate() + occurrence * 7)) { const newEvent = {...event}; const eventDate = new Date(year, month, day); - + // Preserve time portion from original event - const timePart = event.start.includes('T') ? + const timePart = event.start.includes('T') ? event.start.substring(event.start.indexOf('T')) : ''; - const endTimePart = event.end.includes('T') ? + const endTimePart = event.end.includes('T') ? event.end.substring(event.end.indexOf('T')) : ''; - + newEvent.start = formatDateForICS(eventDate) + timePart; newEvent.end = formatDateForICS(eventDate) + endTimePart; expandedEvents.push(newEvent); @@ -168,7 +174,7 @@ document.addEventListener('DOMContentLoaded', function() { }); } } - + return expandedEvents.length > 0 ? expandedEvents : [event]; } @@ -200,7 +206,7 @@ document.addEventListener('DOMContentLoaded', function() { function updateEventsForMonth(year, month) { // Clear existing events for this month view eventsByDate = {}; - + // Process each event, expanding recurring ones events.forEach(ev => { if (ev.rrule) { @@ -222,7 +228,7 @@ document.addEventListener('DOMContentLoaded', function() { // Check if this event belongs to current month view const eventYear = parseInt(dateKey.split('-')[0]); const eventMonth = parseInt(dateKey.split('-')[1]) - 1; - + if (eventYear === year && eventMonth === month) { if (!eventsByDate[dateKey]) { eventsByDate[dateKey] = []; @@ -232,7 +238,7 @@ document.addEventListener('DOMContentLoaded', function() { } } }); - + renderCalendar(year, month); } @@ -261,16 +267,16 @@ document.addEventListener('DOMContentLoaded', function() { } let cell = document.createElement("td"); cell.innerHTML = "" + day + ""; - + let dayStr = day < 10 ? "0" + day : day; let monthStr = (month + 1) < 10 ? "0" + (month + 1) : (month + 1); let dateKey = year + "-" + monthStr + "-" + dayStr; - + if (eventsByDate[dateKey]) { let dotsContainer = document.createElement("div"); dotsContainer.className = "event-dots-container"; cell.classList.add("has-event"); - + // Gruppe Events nach Typ const events = eventsByDate[dateKey]; const hasMembersOnly = events.some(e => e.summary.toLowerCase().includes("members only")); @@ -279,12 +285,12 @@ document.addEventListener('DOMContentLoaded', function() { const hasSpieleabend = events.some(e => e.summary.toLowerCase().includes("spieleabend")); const hasRegular = events.some(e => { const title = e.summary.toLowerCase(); - return !title.includes("members only") && - !title.includes("subbotnik") && - !title.includes("bastelabend") && + return !title.includes("members only") && + !title.includes("subbotnik") && + !title.includes("bastelabend") && !title.includes("spieleabend"); }); - + // Füge Dots entsprechend der Event-Typen hinzu if (hasMembersOnly) { let dot = document.createElement("div"); @@ -301,7 +307,7 @@ document.addEventListener('DOMContentLoaded', function() { dot.className = "event-dot event-dot-greenyellow"; dotsContainer.appendChild(dot); } - + cell.appendChild(dotsContainer); cell.dataset.dateKey = dateKey; cell.addEventListener("click", function() { @@ -330,8 +336,8 @@ document.addEventListener('DOMContentLoaded', function() { if (match && match[1]) { return `https://berlin.ccc.de/datengarten/${match[1]}/`; } - } - + } + // For other titles, convert to lowercase and use as path const slug = eventTitle.toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]/g, ''); return `https://berlin.ccc.de/page/${slug}/`; @@ -349,13 +355,13 @@ document.addEventListener('DOMContentLoaded', function() { let eventTitle = document.createElement("div"); eventTitle.className = "event-title"; - + // Create a link for the event title let titleLink = document.createElement("h"); titleLink.textContent = ev.summary; titleLink.target = "_blank"; eventTitle.appendChild(titleLink); - + eventItem.appendChild(eventTitle); let eventTime = document.createElement("div"); @@ -366,7 +372,7 @@ document.addEventListener('DOMContentLoaded', function() { if (ev.description) { let eventDescription = document.createElement("div"); eventDescription.className = "event-description"; - + // Check if the description is a URL and make it a clickable link if (ev.description.trim().startsWith('http')) { let linkElement = document.createElement("a"); @@ -378,7 +384,7 @@ document.addEventListener('DOMContentLoaded', function() { } else { eventDescription.textContent = ev.description; } - + eventItem.appendChild(eventDescription); } @@ -403,7 +409,7 @@ document.addEventListener('DOMContentLoaded', function() { function formatTime(icsTimeStr) { // Format time for display if (!icsTimeStr) return ""; - + if (icsTimeStr.length === 8) { // All-day event return "Ganztägig"; @@ -425,20 +431,18 @@ document.addEventListener('DOMContentLoaded', function() { } // ICS-Datei abrufen und Events verarbeiten - fetch('/all.ics') + fetch('/calendars/all.ics') .then(response => response.text()) .then(data => { events = parseICS(data); - + // Initialize with current date let today = new Date(); currentYear = today.getFullYear(); currentMonth = today.getMonth(); - + // Process events for current month - - updateEventsForMonth(currentYear, currentMonth); - + updateEventsForMonth(currentYear, currentMonth); }) .catch(err => console.error('Fehler beim Laden der ICS-Datei:', err)); })(); diff --git a/assets/js/clubstatus.js b/assets/js/clubstatus.js index d87eb04..fffd93d 100644 --- a/assets/js/clubstatus.js +++ b/assets/js/clubstatus.js @@ -9,11 +9,11 @@ function timeDistanceDE(pastDate) { const hours = Math.floor(minutes / 60) const days = Math.floor(hours / 24) - if (days = 1) return `since ${days}d` + if (days === 1) return `since ${days}d` if (days > 1) return `since ${days}d` - if (hours = 1) return `since ${hours}h` + if (hours === 1) return `since ${hours}h` if (hours > 1) return `since ${hours}h` - if (minutes = 1) return `since ${minutes}min` + if (minutes === 1) return `since ${minutes}min` if (minutes > 1) return `since ${minutes}min` return `gerade eben` } diff --git a/build.sh b/build.sh index 6bc707a..625a262 100755 --- a/build.sh +++ b/build.sh @@ -1,12 +1,7 @@ #!/bin/sh -set -e -set -x +set -ex rm -rf public/ # delete old generated files hugo $(cat .hugo-params) -./tools/merge_cals.py -upcoming="$(tools/gen_upcoming.py static/all.ics 20 5 | tr '\n' ' ')" -cp static/all.ics public/all.ics -sed -i "s#CALENDAR#$upcoming#g" public/index.html diff --git a/config/_default/hugo.toml b/config/_default/hugo.toml index 13192d6..218ffbb 100644 --- a/config/_default/hugo.toml +++ b/config/_default/hugo.toml @@ -28,7 +28,7 @@ filename = "sitemap.xml" priority = 0.5 [permalinks] -post = "/post/:year/:month/:day/:title/" +post = "/post/:year/:month/:day/:slug/" [outputs] home = ["html", "rss", "json"] diff --git a/content/_index.md b/content/_index.md index 531fa85..4d73f01 100644 --- a/content/_index.md +++ b/content/_index.md @@ -25,6 +25,9 @@ description: "Startseite CCCB mit Kurzkalender" ### Nächste Veranstaltungen -CALENDAR + +
Weitere Termine findest du im [Veranstaltungskalender](/verein/calendar/). + + diff --git a/content/post/2026-07-04-kein-diday-im-cccb/index.md b/content/post/2026-07-04-kein-diday-im-cccb/index.md new file mode 100644 index 0000000..d64dd4b --- /dev/null +++ b/content/post/2026-07-04-kein-diday-im-cccb/index.md @@ -0,0 +1,15 @@ +--- +title: "Kein DI.Day am 5. Juli im CCCB" +date: 2026-07-04T12:00:00+02:00 +showHero: false +tags: ["di.day", "Ankündigung"] +--- + +**Kurzfristige Änderung:** Der [Digital Independence Day](/veranstaltungen/didit/) findet am **Sonntag, dem 5. Juli 2026, nicht** im Chaos Computer Club Berlin (Marienstraße 11) statt. + +Wer trotzdem am DI.Day teilnehmen und den Weg in die digitale Unabhängigkeit weitergehen möchte, ist herzlich bei den anderen Berliner Veranstaltungen willkommen: + +- [„Let's DID it!" mit Linux & Sommerfest der BELUG](https://f.termine.di.day/events/ee48fdda-a134-4f27-932e-b87c7a9fa24a) in der c-base +- [DI.Day im Stadtschloss Moabit](https://f.termine.di.day/events/41ab17fe-05ab-4308-9abb-d38d9485fd4f) + +Weitere Termine und Orte findet ihr wie immer auf [termine.di.day](https://termine.di.day/). diff --git a/content/post/2026-08-23-aktionstag-ueberwachung/index.md b/content/post/2026-08-23-aktionstag-ueberwachung/index.md new file mode 100644 index 0000000..34b1320 --- /dev/null +++ b/content/post/2026-08-23-aktionstag-ueberwachung/index.md @@ -0,0 +1,57 @@ +--- +title: "Aktionstag gegen Überwachung" +date: 2026-08-11T12:00:00+02:00 +showHero: false +tags: ["Überwachung", "Ankündigung"] +--- + +### Was? + +KI-gestützte Überwachungskameras sollen jetzt auch nach Berlin kommen. +Das [im Dezember 2025 verschärfte Berliner Polizeigesetz](https://www.beck-aktuell.de/heute-im-recht/rechtspolitik-gesetzgebung/berlin-verschaerfung-polizeigesetz-2025-12-04) erlaubt der Polizei, solche Kameras an öffentlichen Plätzen aufzuhängen. Erste Versuche laufen schon. + +Darüber wollen wir reden. +Deshalb laden wir am 23. August zum Aktionstag gegen Überwachung in den Club ein. + +### Worum geht es? + +Die KI-Videoüberwachung soll [ein Jahr lang an sechs sogenannten „kriminalitätsbelasteten Orten" getestet werden](https://taz.de/KI-Videoueberwachung-in-Berlin/!6160851/), darunter am Alexanderplatz, am Roten Rathaus und am Alten Stadthaus. Vor dem Abgeordnetenhaus kommt mobile Technik dazu. + +Diese Kameras zeichnen nicht nur auf. Sogenannte Verhaltensscanner sollen automatisch melden, wenn sich jemand „auffällig" verhält. Trainiert wird das Ganze [mit den Daten von Passant:innen, die einfach nur vorbeilaufen](https://netzpolitik.org/2026/ki-gestuetzte-videoueberwachung-in-berlin-so-wehrt-man-sich-gegen-verhaltensscanner/). + +In Mannheim läuft so ein System seit 2018, in Hamburg seit 2023. Dort hält die Software [eine Umarmung schon mal für einen Ringkampf](https://taz.de/Kuenstliche-Intelligenz-im-Einsatz/!6099559/). In Mannheim gab es laut Polizei [jeden Tag Fehlalarme im „niedrigen zweistelligen Bereich"](https://netzpolitik.org/2023/intelligente-videoueberwachung-polizei-hamburg-will-ab-juli-verhalten-automatisch-scannen/). Wer sich hinlegt oder taumelt, löst Alarm aus. Was überhaupt als „auffällig" gilt, entscheidet der Algorithmus. Warum das [Menschen diskriminieren kann, erklärt der Verfassungsblog](https://verfassungsblog.de/berlin-asog-novelle-kbos/). + +Die Kameras sind außerdem nur ein Teil der Gesetzesänderung. Das neue ASOG erlaubt auch [längere Datenspeicherung und weitere Überwachungsbefugnisse](https://www.telepolis.de/article/Berliner-Polizeigesetz-Koalition-weitet-Ueberwachung-aus-11083345.html). Die Berliner Datenschutzbeauftragte hat das kritisiert. Wir auch: Der CCC war dazu bereits im Innenausschuss des Abgeordnetenhauses. + +Ihr braucht kein Vorwissen. Kommt einfach vorbei. + +### Programm + +**13:00 Uhr: Open Space** \ +Wir fangen mit offenem Austausch an. Bringt eure Fragen mit, egal ob ihr euch schon lange mit dem Thema beschäftigt oder gerade zum ersten Mal davon hört. + +**15:00 Uhr: Vortrag** \ +Was in Berlin geplant ist, was die Technik wirklich kann und was das für uns alle heißt. + +**16:00 Uhr: Podiumsdiskussion** \ +Danach diskutieren wir auf dem Podium, wie wir uns gegen die KI-Videoüberwachung wehren können. Fragen aus dem Publikum sind ausdrücklich erwünscht. + +### Wo? + +📍 **Chaos Computer Club Berlin, Marienstraße 11, 10117 Berlin** + +🚈 Etwa 5 Minuten ab dem S-Bahnhof Friedrichstraße + +### Wann? + +**📆 Sonntag, 23. August 2026** \ +**ab 13:00 Uhr** + +### Zum Weiterlesen + +- taz: [KI-Videoüberwachung in Berlin: Verhaltensscanner bald auch vor dem Abgeordnetenhaus](https://taz.de/KI-Videoueberwachung-in-Berlin/!6160851/) +- netzpolitik.org: [So wehrt man sich gegen Verhaltensscanner](https://netzpolitik.org/2026/ki-gestuetzte-videoueberwachung-in-berlin-so-wehrt-man-sich-gegen-verhaltensscanner/) (mit Tools und Material für die Zivilgesellschaft) +- Verfassungsblog: [„Try harder hilft selten": die ASOG-Novelle verfassungsrechtlich eingeordnet](https://verfassungsblog.de/berlin-asog-novelle-kbos/) +- taz: [Reform des Berliner Polizeigesetzes: Riskantes Manöver](https://taz.de/Reform-des-Berliner-Polizeigesetzes/!6096087/) +- Telepolis: [Berliner Polizeigesetz: Koalition weitet Überwachung aus](https://www.telepolis.de/article/Berliner-Polizeigesetz-Koalition-weitet-Ueberwachung-aus-11083345.html) +- beck-aktuell: [Kameras und KI-Einsatz: Berlin verschärft Polizeigesetz](https://www.beck-aktuell.de/heute-im-recht/rechtspolitik-gesetzgebung/berlin-verschaerfung-polizeigesetz-2025-12-04) diff --git a/content/post/Neujahresempfang-2026/index.md b/content/post/Neujahresempfang-2026/index.md index 61844e1..17a6269 100644 --- a/content/post/Neujahresempfang-2026/index.md +++ b/content/post/Neujahresempfang-2026/index.md @@ -1,5 +1,6 @@ --- title: "Der Neujahresempfang 2026 #NJE26" +slug: "neujahresempfang-2026" date: 2026-03-05T12:00:00+01:00 showHero: false tags: ["NJE", "Ankündigung"] diff --git a/content/veranstaltungen/aktionstag-ueberwachung.md b/content/veranstaltungen/aktionstag-ueberwachung.md new file mode 100644 index 0000000..6976864 --- /dev/null +++ b/content/veranstaltungen/aktionstag-ueberwachung.md @@ -0,0 +1,23 @@ +--- +title: "Aktionstag gegen Überwachung" +subtitle: "KI-Überwachungskameras in Berlin: Vortrag, Podium und Austausch" +date: 2026-08-11T12:00:00+02:00 +dtstart: 20260823T130000 +dtend: 20260823T180000 +menu: + main: + parent: "Veranstaltungen" +tag: ["Veranstaltung"] +--- + +**Am 23. August lädt der Chaos Computer Club Berlin zum Aktionstag gegen Überwachung.** + +KI-gestützte Überwachungskameras sollen jetzt auch nach Berlin kommen. Das neue Berliner Polizeigesetz erlaubt es, sie an öffentlichen Plätzen aufzuhängen. Erste Versuche laufen schon, Verhaltenstracking inklusive. Darüber wollen wir reden. + +Das Programm: + +- **13:00 Uhr:** Open Space, offener Austausch untereinander +- **15:00 Uhr:** Vortrag zu den KI-Kameras und dem neuen Berliner Polizeigesetz +- **16:00 Uhr:** Podiumsdiskussion + +📍 Chaos Computer Club Berlin, Marienstraße 11, 10117 Berlin diff --git a/content/veranstaltungen/bastelabend.md b/content/veranstaltungen/bastelabend.md index 5f3d995..c787c17 100644 --- a/content/veranstaltungen/bastelabend.md +++ b/content/veranstaltungen/bastelabend.md @@ -26,7 +26,7 @@ draft: true {{< alert "circle-info" >}} -Wenn ihr neu seid und den CCCB zum ersten Mal besuchen wollt, kommt am besten an einem Donnerstag zum [Club Discordia](/page/clubdiscordia/), da samstags nicht immer genug Leute da sind, um euch zu empfangen. +Wenn ihr neu seid und den CCCB zum ersten Mal besuchen wollt, kommt am besten an einem Donnerstag zum [Club Discordia](/veranstaltungen/clubdiscordia/), da samstags nicht immer genug Leute da sind, um euch zu empfangen. -Generell sind aber alle, die schonmal im Club waren, herzlich eingeladen, an den Bastelabenden vorbeizukommen. +Generell sind aber alle, die schon mal im Club waren, herzlich eingeladen, an den Bastelabenden vorbeizukommen. {{< /alert >}} diff --git a/content/veranstaltungen/didit.md b/content/veranstaltungen/didit.md new file mode 100644 index 0000000..de8ddc6 --- /dev/null +++ b/content/veranstaltungen/didit.md @@ -0,0 +1,22 @@ +--- +title: "Di.Day" +subtitle: "Digital Independence Day im CCCB" +date: 2026-01-04T12:23:00+01:00 +dtstart: 20260104T122300 +dtend: 20260104T174200 +rrule: "FREQ=MONTHLY;INTERVAL=2;BYDAY=1SU;WKST=MO;UNTIL=20260503T235959Z" +menu: + main: + parent: "Veranstaltungen" +tag: ["Veranstaltung"] +--- + +In Berlin wird der [Digital Independence Day](https://diday.org) an verschiedenen Orten von einer breiten Zahl von Initiativen getragen. + +Der CCCB beteiligt sich an dieser Graswurzelbewegung und ruft dazu auf, zu demokratiefreundlichen digitalen Alternativen zu wechseln, um die Abhängigkeit von großen Technologiekonzernen zu verringern. + +Am ersten Sonntag im Monat gibt es praktische Hilfe beim Wechsel zu datenschutzfreundlichen Diensten, beim Einrichten von Linux oder beim Betrieb von Smartphones ohne große Tech-Konzerne. Egal, ob Anfänger:in oder Fortgeschrittene:r. Kommt vorbei und bringt eure Geräte und Fragen mit. + +Die Aktiven im CCCB unterstützen dabei auf verschiedenen Veranstaltungen in Berlin. + +Weitere Informationen zu konkreten Veranstaltungen gibt es im Terminkalender unter [https://events.diday.org/](https://events.diday.org/) bzw [https://events.diday.org/organisation/cccb](https://events.diday.org/organisation/cccb). diff --git a/content/veranstaltungen/neujahresempfang.md b/content/veranstaltungen/neujahresempfang.md index 6bcd354..98de614 100644 --- a/content/veranstaltungen/neujahresempfang.md +++ b/content/veranstaltungen/neujahresempfang.md @@ -2,13 +2,12 @@ title: "Neujahresempfang" subtitle: "#NJE 26" date: 2025-03-05T18:00:00+02:00 -dtstart: 20250523T180000 -dtend: 20250524T100000 -rrule: "FREQ=MOTHLY;BYDAY=4SA;WKST=MO" +dtstart: 20260523T180000 +dtend: 20260524T100000 menu: main: parent: "Veranstaltungen" tag: ["Veranstaltung"] --- -Am 23. Mai lädt der Chaos Computer Club Berlin zum Neujahresempfang. [Hier geht es zur Infoseite!](https://berlin.ccc.de/post/2026/03/05/der-neujahresempfang-2026-%23nje26/) \ No newline at end of file +Am 23. Mai lädt der Chaos Computer Club Berlin zum Neujahresempfang. [Hier geht es zur Infoseite!](https://berlin.ccc.de/post/2026/03/05/neujahresempfang-2026/) \ No newline at end of file diff --git a/content/veranstaltungen/openwrt.md b/content/veranstaltungen/openwrt.md index c2e66e8..282c130 100644 --- a/content/veranstaltungen/openwrt.md +++ b/content/veranstaltungen/openwrt.md @@ -2,9 +2,9 @@ title: "OpenWrt" subtitle: "OpenWrt Stammtisch" date: 2025-01-08T20:00:00+02:00 -dtstart: 20250115T200000 -dtend: 20250115T230000 -rrule: "FREQ=MONTHLY;BYDAY=3WE;WKST=MO" +dtstart: 20251119T200000 +dtend: 20251119T230000 +rrule: "FREQ=MONTHLY;INTERVAL=3;BYDAY=3WE;WKST=MO" menu: main: parent: "Veranstaltungen" @@ -13,7 +13,7 @@ tag: ["Veranstaltung"] ![Verschiedene Platinen im CCCB](/img/club/42300970272_667569d239.jpg) -**Jeden 3. Mittwoch im Monat ab 20 Uhr** treffen sich die OpenWrt begeisterten und die es werden wollen zum OpenWrt Stammtisch im CCCB. +**Jeden 3. Mittwoch im Februar, Mai, August und November ab 20 Uhr** treffen sich die OpenWrt begeisterten und die es werden wollen zum OpenWrt Stammtisch im CCCB. Das OpenWrt Meetup richtet sich an alle die OpenWrt benutzen oder benutzen wollen aber auch Leute die an OpenWrt entwickeln. diff --git a/content/veranstaltungen/spieleabend.md b/content/veranstaltungen/spieleabend.md index e1122f9..ab3d120 100644 --- a/content/veranstaltungen/spieleabend.md +++ b/content/veranstaltungen/spieleabend.md @@ -16,7 +16,7 @@ tag: ["Veranstaltung"] **Jeden 2. und 4. Samstag im Monat ist ab 17 Uhr Spieleabend im Club.** {{< alert "circle-info" >}} -Wenn ihr neu seid und den CCCB zum ersten Mal besuchen wollt, kommt am besten an einem Donnerstag zum [Club Discordia](/page/clubdiscordia/), da samstags nicht immer genug Leute da sind, um euch zu empfangen. +Wenn ihr neu seid und den CCCB zum ersten Mal besuchen wollt, kommt am besten an einem Donnerstag zum [Club Discordia](/veranstaltungen/clubdiscordia/), da samstags nicht immer genug Leute da sind, um euch zu empfangen. -Generell sind aber alle, die schonmal im Club waren, herzlich eingeladen, an den Spieleabenden vorbeizukommen. +Generell sind aber alle, die schon mal im Club waren, herzlich eingeladen, an den Spieleabenden vorbeizukommen. {{< /alert >}} diff --git a/content/veranstaltungen/subbotnik.md b/content/veranstaltungen/subbotnik.md index 5e86aa2..a520035 100644 --- a/content/veranstaltungen/subbotnik.md +++ b/content/veranstaltungen/subbotnik.md @@ -23,8 +23,8 @@ Der Begriff [Subbotnik](https://de.wikipedia.org/wiki/Subbotnik) ist eine in Sow --- {{< alert "circle-info" >}} -Wenn ihr neu seid und den CCCB zum ersten Mal besuchen wollt, kommt am besten an einem Donnerstag zum [Club Discordia](/page/clubdiscordia/), da samstags nicht immer genug Leute da sind, um euch zu empfangen. +Wenn ihr neu seid und den CCCB zum ersten Mal besuchen wollt, kommt am besten an einem Donnerstag zum [Club Discordia](/veranstaltungen/clubdiscordia/), da samstags nicht immer genug Leute da sind, um euch zu empfangen. -Generell sind aber alle, die schonmal im Club waren, herzlich eingeladen am Subbotnik vorbeizukommen und mitzuhelfen. +Generell sind aber alle, die schon mal im Club waren, herzlich eingeladen am Subbotnik vorbeizukommen und mitzuhelfen. {{< /alert >}} diff --git a/content/verein/calendar/index.md b/content/verein/calendar/index.md index fdac4e4..3fccf38 100644 --- a/content/verein/calendar/index.md +++ b/content/verein/calendar/index.md @@ -12,4 +12,15 @@ herostyle: big {{< calendar >}} -Keinen Termin mehr verpeilen? Einfach den [Veranstaltungskalender abonnieren](/all.ics)! +Keinen Termin mehr verpeilen? Einfach den [Veranstaltungskalender abonnieren](/calendars/all.ics)! + +Oder willst du nur den Kalender von bestimmten Events? + +- [Club Discordia](/calendars/Club_Discordia.ics) +- [Neujahresempfang](/calendars/Neujahresempfang.ics) +- [Datengarten](/calendars/Datengarten.ics) +- [Spieleabend](/calendars/Spieleabend.ics) +- [OpenWRT](/calendars/OpenWRT.ics) +- [Plenum](/calendars/Plenum.ics) +- [Amateurfunk](/calendars/Amateurfunk.ics) +- [Irreguläre Events](/calendars/Random.ics) diff --git a/flake.lock b/flake.lock index 2869045..33ab9db 100644 --- a/flake.lock +++ b/flake.lock @@ -2,16 +2,16 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1748995628, - "narHash": "sha256-bFufQGSAEYQgjtc4wMrobS5HWN0hDP+ZX+zthYcml9U=", + "lastModified": 1786313170, + "narHash": "sha256-9BG7OgUWdu0ONDO5X2q6+K4bsuBITkX/3W4nNJu1Ito=", "owner": "nixos", "repo": "nixpkgs", - "rev": "8eb3b6a2366a7095939cd22f0dc0e9991313294b", + "rev": "fcb8fcd6bf2d0adecae5bd491afaaaf8311b758d", "type": "github" }, "original": { "owner": "nixos", - "ref": "nixos-24.11", + "ref": "nixos-26.05", "repo": "nixpkgs", "type": "github" } diff --git a/flake.nix b/flake.nix index 39d9b6d..c29b73a 100644 --- a/flake.nix +++ b/flake.nix @@ -2,7 +2,7 @@ description = "A flake containing a development environment for the CCCB website."; inputs = { - nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11"; + nixpkgs.url = "github:nixos/nixpkgs/nixos-26.05"; }; outputs = @@ -34,7 +34,7 @@ devShells = forAllSystems (import ./devShells.nix); - formatter = forAllSystems ({ pkgs, ... }: pkgs.nixfmt-rfc-style); + formatter = forAllSystems ({ pkgs, ... }: pkgs.nixfmt-tree); lib.mkWwwContent = { domain, system }: @@ -50,10 +50,6 @@ nativeBuildInputs = [ pkgs.hugo pkgs.glibcLocales - (pkgs.python3.withPackages (python-pkgs: [ - python-pkgs.icalendar - python-pkgs.pytz - ])) ]; # LOCALE_ARCHIVE = builtins.trace pkgs.glibcLocales "${pkgs.glibcLocales}/lib/locale/locale-archive"; @@ -62,11 +58,6 @@ buildPhase = '' mkdir -p public hugo --baseURL=https://${domain}/ - - python3 ./tools/merge_cals.py - upcoming="$(python3 tools/gen_upcoming.py static/all.ics 20 5 | tr '\n' ' ')" - cp static/all.ics public/all.ics - sed -i "s#CALENDAR#$upcoming#g" public/index.html ''; # Install phase - copy the public directory to the output diff --git a/layouts/_default/baseof.calendar.html b/layouts/_default/baseof.calendar.html deleted file mode 100644 index bddf04a..0000000 --- a/layouts/_default/baseof.calendar.html +++ /dev/null @@ -1 +0,0 @@ -{{ block "main" . }}{{ .Content }}{{ end }} diff --git a/layouts/_default/list.calendar.html b/layouts/_default/list.calendar.html deleted file mode 100644 index 3259690..0000000 --- a/layouts/_default/list.calendar.html +++ /dev/null @@ -1,21 +0,0 @@ -BEGIN:VCALENDAR -VERSION:2.0 -PRODID:-//CCCB//Calendar//DE -{{ with .Title }} -X-WR-CALNAME:{{ . }} -{{ end }} -CALSCALE:GREGORIAN -METHOD:PUBLISH -{{ range .Pages }} -{{ if .Date }} -BEGIN:VEVENT -UID:{{ .File.UniqueID }}@berlin.ccc.de -DTSTAMP:{{ .Date.Format "20060102T150405Z" }} -DTSTART:{{ .Date.Format "20060102T150405Z" }} -SUMMARY:{{ .Title }} -DESCRIPTION:{{ .Summary | plainify }} -URL:{{ .Permalink }} -END:VEVENT -{{ end }} -{{ end }} -END:VCALENDAR diff --git a/layouts/_default/list.xml.html b/layouts/_default/list.xml.xml similarity index 100% rename from layouts/_default/list.xml.html rename to layouts/_default/list.xml.xml diff --git a/layouts/_default/single.calendar.html b/layouts/_default/single.calendar.html deleted file mode 100644 index 5a22ec4..0000000 --- a/layouts/_default/single.calendar.html +++ /dev/null @@ -1,21 +0,0 @@ -BEGIN:VCALENDAR -VERSION:2.0 -PRODID:-//CCCB//Calendar//DE -{{ with .Title }} -X-WR-CALNAME:{{ . }} -{{ end }} -CALSCALE:GREGORIAN -METHOD:PUBLISH -{{ if .Date }} -BEGIN:VEVENT -UID:{{ .File.UniqueID }}@berlin.ccc.de -DTSTAMP:{{ .Date.Format "20060102T150405Z" }} -DTSTART:{{ .Date.Format "20060102T150405Z" }} -{{ with .Params.event.end }}DTEND:{{ dateFormat "20060102T150405Z" . }}{{ end }} -SUMMARY:{{ .Title }} -DESCRIPTION:{{ .Summary | plainify }} -URL:{{ .Permalink }} -{{ with .Params.location }}LOCATION:{{ . }}{{ end }} -END:VEVENT -{{ end }} -END:VCALENDAR diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index c61ba7f..0000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -icalendar==5.0.7 -pytz \ No newline at end of file diff --git a/static/js/upcoming.js b/static/js/upcoming.js new file mode 100644 index 0000000..6b7a711 --- /dev/null +++ b/static/js/upcoming.js @@ -0,0 +1,113 @@ +import ICAL from "https://unpkg.com/ical.js/dist/ical.min.js"; + +/** + * Parse an ICS calendar and return upcoming event occurrences. + * + * @param {string} icsText The contents of the .ics file + * @param {Date} now Events must start after this date + * @param {number} maxEvents Maximum number of events to return + * @param {number} maxDays Maximum number of days into the future + * @returns {{start: Date, name: string, url: string}[]} + */ +function getUpcomingEvents(icsText, now, maxEvents, maxDays) { + const jcal = ICAL.parse(icsText); + const calendar = new ICAL.Component(jcal); + + const end = new Date(now.getTime()); + end.setDate(end.getDate() + maxDays); + + const events = []; + + for (const component of calendar.getAllSubcomponents("vevent")) { + const event = new ICAL.Event(component); + + if (!event.startDate) { + continue; + } + + if (event.isRecurring()) { + const iterator = event.iterator(); + + while (true) { + const occurrence = iterator.next(); + + if (!occurrence) { + break; + } + + const start = occurrence.toJSDate(); + + // Recurrences are chronological, so we're done + // once we pass the end of our search window. + if (start > end) { + break; + } + + if (start > now) { + events.push({ + start, + name: event.summary ?? "", + url: event.url ?? "", + }); + } + } + } else { + const start = event.startDate.toJSDate(); + + if (start > now && start <= end) { + events.push({ + start, + name: event.summary ?? "", + url: event.url ?? "", + }); + } + } + } + + // We have occurrences from multiple events, so sort them + // before applying the maximum event count. + events.sort((a, b) => a.start - b.start); + + return events.slice(0, maxEvents); +} + +document.addEventListener("DOMContentLoaded", () => { + const ics = "/calendars/all.ics"; + const max_days = 20; + const max_items = 5; + + const now = new Date(); + + const table = document.getElementById("upcoming"); + fetch(ics) + .then(response => response.text()) + .then(icsText => { + getUpcomingEvents(icsText, now, max_items, max_days).forEach(event => { + const row = document.createElement("tr"); + + const colBegin = document.createElement("td"); + + const formattedStart = event.start.toLocaleString("de-DE", { + weekday: "long", + day: "2-digit", + month: "2-digit", + hour: "2-digit", + minute: "2-digit", + }).replace(",", ""); + + colBegin.innerText = `${formattedStart.replace(" ", ", ")} Uhr`; + row.appendChild(colBegin); + + const colName = document.createElement("td"); + + const a = document.createElement("a"); + a.href = event.url; + a.text = event.name; + + colName.appendChild(a); + row.appendChild(colName); + + table.appendChild(row); + }); + }); +}); diff --git a/tools/gen_upcoming.py b/tools/gen_upcoming.py deleted file mode 100755 index 0f69f6a..0000000 --- a/tools/gen_upcoming.py +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/env python3 - -import sys -import logging -import locale -from dateutil.parser import parse -from datetime import datetime, timedelta -from dateutil.rrule import rruleset, rrulestr - -import icalendar - - -def vevent_to_event(event, rrstart=None): - if rrstart == None: - begin = parse(event["DTSTART"].to_ical()) - else: - begin = rrstart - - return { - "name": event["SUMMARY"].to_ical(), - "url": event["URL"].to_ical(), - "begin": begin - } - - -def parse_single_event(event, start, end): - logging.info(f"Processing single event {event['SUMMARY'].to_ical().decode('utf-8')}") - dtstart = parse(event["DTSTART"].to_ical()) - if dtstart >= start and dtstart < end: - return vevent_to_event(event) - - -def parse_recurring_event(event, start, end): - logging.info(f"Processing recurring event {event['SUMMARY'].to_ical().decode('utf-8')}") - dtstart = parse(event["DTSTART"].to_ical()) - rs = rruleset() - rs.rrule(rrulestr(event["RRULE"].to_ical().decode("utf-8"), dtstart=dtstart)) - if "EXDATE" in event.keys(): - for exdate in event["EXDATE"]: - rs.exdate(parse(exdate.to_ical())) - - events = [] - for date in list(rs): - if date >= start and date < end: - events.append(vevent_to_event(event, date)) - - return events - - -def find_events(icsfilestr, start, end, num): - with open(icsfilestr, "r") as icsfile: - cal = icalendar.Calendar.from_ical(icsfile.read()) - - events = [] - for event in cal.subcomponents: - if event.name == "VEVENT": - if "RRULE" in event.keys(): - events.extend(parse_recurring_event(event, start, end)) - else: - ev = parse_single_event(event, start, end) - if ev is not None: - events.append(ev) - - events = sorted(events, key=lambda k: k["begin"]) - events = events[0:num] - - return events - - -def format_events(events): - print("") - for event in events: - print( - "" - f"" - f"" - "" - ) - print("
{event['begin'].strftime('%A, %d.%m um %H:%M Uhr')}{event['name'].decode('utf-8')}
") - - -if __name__ == "__main__": - if len(sys.argv) < 3: - print(f"Usage: {sys.argv[0]} calendar max_days max_items") - sys.exit(-1) - - locale.setlocale(locale.LC_TIME, "de_DE.UTF-8") - calendar = sys.argv[1] - max_days = int(sys.argv[2]) - max_items = int(sys.argv[3]) - - now = datetime.now() - events = find_events(calendar, now, now + timedelta(days=max_days), max_items) - format_events(events) - diff --git a/tools/merge_cals.py b/tools/merge_cals.py deleted file mode 100755 index 136904e..0000000 --- a/tools/merge_cals.py +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env python3 - -from glob import glob - -import pytz -import icalendar - - -calendars = [] -merged = icalendar.Calendar() -merged.add("prodid", "-//CCCB Calendar Generator//berlin.ccc.de//") -merged.add("version", "2.0") - -for icsfilestr in glob("public/*/**/*.ics", recursive=True): - with open(icsfilestr, "r") as icsfile: - print(f"Importing {icsfilestr}") - calendars.append(icalendar.Calendar.from_ical(icsfile.read())) - -for calendar in calendars: - for event in calendar.subcomponents: - if event.name != "VEVENT": - continue - if "DTSTART" not in event: - continue - merged.add_component(event) - -outfile = "static/all.ics" -with open(outfile, "wb") as f: - print(f"writing to {outfile}...") - f.write(merged.to_ical()) -