# Operation Calendar

An operation calendar app for Alliance Auth to display fleet operations and other events.

![release](https://img.shields.io/pypi/v/aa-opcalendar??label=release) ![python](https://img.shields.io/pypi/pyversions/aa-opcalendar?) ![license](https://img.shields.io/badge/license-MIT-green)

## Includes

* Calendar type view of different events
* Manual events
  * User created
  * Detailed view
  * Ical feed for exporting events
* Public NPSI events
  * Automatic syncing with supported NPSI community events over APIs
* Ingame events
  * Automatic syncing with ingame events
  * Personal, Corporation and Alliance calendars
* Supports [structure timers](https://gitlab.com/ErikKalkoken/aa-structuretimers)
* Supports [aa-moonmining](https://gitlab.com/ErikKalkoken/aa-moonmining)
* Self-hosted Discord bot to fetch events and sign up over Discord
* Event visibility options
  * Custom names and colors
  * Restrict to groups
  * Restrict to states
  * Webhook for sending event notifications
  * Filter to include in ical feed
* Event categories
  * Custom names
  * Custom tickers
  * Custom colors
  * Optional FontAwesome icons and fallback color dots on the calendar
  * Pre-fill text to add on events with the category
* Multihost support
* Discord notifications
  * Webhook
  * For: new, edited and deleted events
  * Configurable ping timing (now / 1h / 1d / 1w before / custom)
* "Important" events with a blinking indicator on the calendar (and a 🔴 marker in Discord)
* Discord event channel bot
  * Posts and keeps a pinned summary embed of upcoming events (including ingame events) in sync in a Discord channel
  * `/signup <number> [attending|maybe|declined]` slash command to sign up for events directly from Discord
* Counter on menu for events that the user has not signed or rejected from

<table>
  <tr>
    <td align="center" width="50%">
      <img src="docs/images/calendar-dark.jpg" width="400" height="240" alt="Calendar - dark theme"><br>
      Dark theme
    </td>
    <td align="center" width="50%">
      <img src="docs/images/calendar-light.jpg" width="400" height="240" alt="Calendar - light theme"><br>
      Light theme
    </td>
  </tr>
  <tr>
    <td align="center">
      <img src="docs/images/event-details.jpg" width="400" height="240" alt="Event details"><br>
      Details for manual events
    </td>
    <td align="center">
      <img src="docs/images/ops-list.jpg" width="400" height="240" alt="Event details"><br>
      Event list command
    </td>
  </tr>
  <tr>
    <td align="center">
      <img src="docs/images/discord-summary.jpg" width="400" height="240" alt="aa-discordbot support"><br>
      Event treacker in discord channel
    </td>
  </tr>
</table>

---

## Requirements

* Alliance Auth 4.x or 5.x
* Python >= 3.10
* django-esi >= 8.0.0
* `py-cord>=2.0,<3.0` (only needed if you use the [Discord event channel bot](#discord-event-channel-bot))

## Installation

### Standard (venv / supervisor)

1. Install the package:
   ```bash
   pip install aa-opcalendar
   ```
2. Add `'opcalendar'` to your `INSTALLED_APPS` in your project's `local.py`
3. Run migrations:
   ```bash
   python manage.py migrate
   ```
4. Collect static files:
   ```bash
   python manage.py collectstatic
   ```
5. Restart supervisor:
   ```bash
   supervisorctl restart myauth:
   ```
6. Set up [permissions](#permissions)

#### Optional: Importing NPSI fleets

Opcalendar can automatically import predetermined NPSI fleets directly into
your calendar from public NPSI community APIs. This is **entirely
optional** — skip this if you don't need it.

1. Go to the admin panel and select **NPSI Event Imports**.
2. Create a host for each import and fill in the needed details for it.
3. Add a new import by pressing the add event import button, then select
   the source, operation type and operation visibility for each fetched
   fleet.
4. Add the following to your `local.py` (or set up a periodic task for
   `opcalendar.tasks.import_all_npsi_fleets` in the admin menu) to run
   imports hourly:
   ```python
   CELERYBEAT_SCHEDULE['import_all_npsi_fleets'] = {
       'task': 'opcalendar.tasks.import_all_npsi_fleets',
       'schedule': crontab(minute=0, hour='*'),
   }
   ```
5. Restart supervisor for the schedule change to take effect:
   ```bash
   supervisorctl restart myauth:
   ```

See [Importing NPSI fleets](#importing-npsi-fleets) for the list of
supported NPSI communities.

#### Optional: Importing fleets from ingame calendar

Opcalendar can pull fleets directly from in-game (ESI) personal,
corporation and alliance calendars. This is **entirely optional** — skip
this if you don't need it.

1. Give the `add_ingame_calendar_owner` permission to the wanted groups
   (see [Permissions](#permissions)).
2. Navigate to the opcalendar page and press the `Add Ingame Calendar Feed`
   button, then log in with the character that holds the calendar you want
   to import.
3. Add the following to your `local.py` (or set up a periodic task for
   `opcalendar.tasks.update_all_ingame_events` in the admin menu) to pull
   fleets from ingame every 5 minutes:
   ```python
   CELERYBEAT_SCHEDULE['update_all_ingame_events'] = {
       'task': 'opcalendar.tasks.update_all_ingame_events',
       'schedule': crontab(minute='*/5'),
   }
   ```
4. Restart supervisor for the schedule change to take effect:
   ```bash
   supervisorctl restart myauth:
   ```

See [Importing fleets from ingame calendar](#importing-fleets-from-ingame-calendar)
for how imported events are displayed and how to assign a visibility filter
and category to them.

#### Optional: Ical feed

Opcalendar can generate a standard ical-formatted feed of events for export
into personal calendar apps. This is **entirely optional** — skip this if
you don't need it.

1. Open the relevant event visibility filter/category in the admin and
   check the box to include it in the ical feed. Only categories tagged
   with this box will show up on the feed.
2. Add `'opcalendar'` to `APPS_WITH_PUBLIC_VIEWS` in your `local.py`
   (create the setting if it doesn't exist):
   ```python
   APPS_WITH_PUBLIC_VIEWS = [
       'opcalendar',
   ]
   ```
3. Restart supervisor:
   ```bash
   supervisorctl restart myauth:
   ```
4. The feed is now available at `auth.example.com/opcalendar/feed.ics`

#### Optional: Discord event channel bot

Opcalendar can run a self-hosted Discord bot that posts and keeps a single
pinned summary embed of upcoming events in sync inside a Discord channel,
and lets members sign up via a `/signup` slash command. This is **entirely
optional** — skip this if you don't need it.

1. Create a Discord application and bot user in the [Discord Developer Portal](https://discord.com/developers/applications) and invite it to your server with permission to read/send messages, manage messages (for pinning) and use slash commands.
2. Add the bot token to your `local.py`:
   ```python
   OPCALENDAR_DISCORD_BOT_TOKEN = "your-bot-token-here"
   ```
3. The bot needs to run as its own long-running process. Add a program
   block for it to your supervisor config (e.g.
   `/etc/supervisor/conf.d/myauth.conf`), adjusting the paths to match your
   install:
   ```ini
   [program:opcalendar_event_channel_bot]
   command=/home/allianceserver/venv/auth/bin/python /home/allianceserver/myauth/manage.py run_event_channel_bot
   directory=/home/allianceserver/myauth
   user=allianceserver
   numprocs=1
   autostart=true
   autorestart=true
   stopasgroup=true
   killasgroup=true
   stderr_logfile=/home/allianceserver/myauth/log/opcalendar_event_channel_bot.err.log
   stdout_logfile=/home/allianceserver/myauth/log/opcalendar_event_channel_bot.out.log
   ```

The [Discord ping timing](#discord-ping-timing) feature (1 hour/1 day/1 week before fleet, or a custom date and time) requires a periodic task to actually send those reminders. The "Now" ping does not need this, since it's sent immediately when the event is created.

Add the following to your `local.py` (or set up a periodic task for `opcalendar.tasks.send_scheduled_event_notifications` in the admin menu) so due reminders are checked and sent every 5 minutes:

```python
CELERYBEAT_SCHEDULE['send_scheduled_event_notifications'] = {
    'task': 'opcalendar.tasks.send_scheduled_event_notifications',
    'schedule': crontab(minute='*/5'),
}
```

> [!IMPORTANT]
> Without this task scheduled, only the "Now" ping will be sent — the 1 hour/1 day/1 week and custom-time reminders will silently never fire.

   ```bash
   supervisorctl reread
   supervisorctl update
   supervisorctl restart opcalendar_event_channel_bot
   ```
4. Go to the admin panel and create an **Event Channel**, selecting the
   Discord channel ID and the visibility filter whose events should be
   listed in the summary.

### Docker

1. Add the package to your `conf/requirements.txt`:
   ```
   aa-opcalendar==<latest stable version>
   ```
2. Add `'opcalendar'` to `INSTALLED_APPS` in your `conf/local.py`
3. Rebuild and restart the AllianceAuth containers:
   ```bash
   docker compose build --no-cache
   docker compose up -d
   ```
4. Run migrations and collect static files:
   ```bash
   docker compose exec allianceauth_gunicorn python3 manage.py migrate
   docker compose exec allianceauth_gunicorn python3 manage.py collectstatic --noinput
   ```
5. Set up [permissions](#permissions)

#### Optional: Importing NPSI fleets

Opcalendar can automatically import predetermined NPSI fleets directly into
your calendar from public NPSI community APIs. This is **entirely
optional** — skip this if you don't need it.

1. Go to the admin panel and select **NPSI Event Imports**.
2. Create a host for each import and fill in the needed details for it.
3. Add a new import by pressing the add event import button, then select
   the source, operation type and operation visibility for each fetched
   fleet.
4. Add the following to your `conf/local.py` (or set up a periodic task for
   `opcalendar.tasks.import_all_npsi_fleets` in the admin menu) to run
   imports hourly:
   ```python
   CELERYBEAT_SCHEDULE['import_all_npsi_fleets'] = {
       'task': 'opcalendar.tasks.import_all_npsi_fleets',
       'schedule': crontab(minute=0, hour='*'),
   }
   ```
5. Restart the containers for the schedule change to take effect:
   ```bash
   docker compose restart allianceauth_gunicorn allianceauth_worker allianceauth_beat
   ```

See [Importing NPSI fleets](#importing-npsi-fleets) for the list of
supported NPSI communities.

#### Optional: Importing fleets from ingame calendar

Opcalendar can pull fleets directly from in-game (ESI) personal,
corporation and alliance calendars. This is **entirely optional** — skip
this if you don't need it.

1. Give the `add_ingame_calendar_owner` permission to the wanted groups
   (see [Permissions](#permissions)).
2. Navigate to the opcalendar page and press the `Add Ingame Calendar Feed`
   button, then log in with the character that holds the calendar you want
   to import.
3. Add the following to your `conf/local.py` (or set up a periodic task for
   `opcalendar.tasks.update_all_ingame_events` in the admin menu) to pull
   fleets from ingame every 5 minutes:
   ```python
   CELERYBEAT_SCHEDULE['update_all_ingame_events'] = {
       'task': 'opcalendar.tasks.update_all_ingame_events',
       'schedule': crontab(minute='*/5'),
   }
   ```
4. Restart the containers for the schedule change to take effect:
   ```bash
   docker compose restart allianceauth_gunicorn allianceauth_worker allianceauth_beat
   ```

See [Importing fleets from ingame calendar](#importing-fleets-from-ingame-calendar)
for how imported events are displayed and how to assign a visibility filter
and category to them.

#### Optional: Ical feed

Opcalendar can generate a standard ical-formatted feed of events for export
into personal calendar apps. This is **entirely optional** — skip this if
you don't need it.

1. Open the relevant event visibility filter/category in the admin and
   check the box to include it in the ical feed. Only categories tagged
   with this box will show up on the feed.
2. Add `'opcalendar'` to `APPS_WITH_PUBLIC_VIEWS` in your `conf/local.py`
   (create the setting if it doesn't exist):
   ```python
   APPS_WITH_PUBLIC_VIEWS = [
       'opcalendar',
   ]
   ```
3. Restart the containers:
   ```bash
   docker compose restart allianceauth_gunicorn allianceauth_worker allianceauth_beat
   ```
4. The feed is now available at `auth.example.com/opcalendar/feed.ics`

#### Optional: Discord event channel bot

Opcalendar can run a self-hosted Discord bot that posts and keeps a single
pinned summary embed of upcoming events in sync inside a Discord channel,
and lets members sign up via a `/signup` slash command. This is **entirely
optional** — skip this if you don't need it.

1. Create a Discord application and bot user in the [Discord Developer Portal](https://discord.com/developers/applications) and invite it to your server with permission to read/send messages, manage messages (for pinning) and use slash commands.
2. Add the bot token to your `conf/local.py`:
   ```
   OPCALENDAR_DISCORD_BOT_TOKEN=your-bot-token-here
   ```
4. Add a new service to your `docker-compose.yml`, reusing the
   `x-allianceauth-base` anchor shared by
   `allianceauth_gunicorn`/`allianceauth_beat`/`allianceauth_worker` so it
   inherits the same image, `env_file`, volumes, `depends_on`, working
   directory and logging:
   ```yaml
     allianceauth_opcalendar_bot:
       container_name: allianceauth_opcalendar_bot
       <<: [*allianceauth-base]
       entrypoint: ["python3", "manage.py", "run_event_channel_bot"]
   ```

The [Discord ping timing](#discord-ping-timing) feature (1 hour/1 day/1 week before fleet, or a custom date and time) requires a periodic task to actually send those reminders. The "Now" ping does not need this, since it's sent immediately when the event is created.

Add the following to your `local.py` (or set up a periodic task for `opcalendar.tasks.send_scheduled_event_notifications` in the admin menu) so due reminders are checked and sent every 5 minutes:

```python
CELERYBEAT_SCHEDULE['send_scheduled_event_notifications'] = {
    'task': 'opcalendar.tasks.send_scheduled_event_notifications',
    'schedule': crontab(minute='*/5'),
}
```

> [!IMPORTANT]
> Without this task scheduled, only the "Now" ping will be sent — the 1 hour/1 day/1 week and custom-time reminders will silently never fire.

   ```bash
   docker compose up -d allianceauth_opcalendar_bot
   docker compose logs -f allianceauth_opcalendar_bot
   ```
   > [!NOTE]
   > If `OPCALENDAR_DISCORD_BOT_TOKEN` is unset/blank, this container will
   > start and immediately exit — the bot management command no-ops when
   > the feature is disabled. Set the token first, or don't add this
   > service until you're ready to use the Discord sign-up feature.
5. Go to the admin panel and create an **Event Channel**, selecting the
   Discord channel ID and the visibility filter whose events should be
   listed in the summary.

## Permissions

Perm | Auth Site | Example Target Group
 --- | --- | ---
opcalendar basic_access | Can access this app and see operations based on visibility rules | Everyone
opcalendar create_event | Can create new events and edit/delete their own events | Members, FCs
opcalendar manage_event | Can edit and delete their own events | Members, FCs
opcalendar manage_all_events | Can edit and delete all events, including events created by other users | Leadership, FCs
opcalendar see_signups | Can see all signups for event | Leadership, FCs, Members
opcalendar add_ingame_calendar_owner | Can add ingame calendar feeds for their corporation | Leadership, FCs

## Settings

Name | Description | Default
 --- | --- | ---
OPCALENDAR_NOTIFY_IMPORTS | Whether to send out discord notifications for ingame and public NPSI events | True
OPCALENDAR_DISPLAY_STRUCTURETIMERS | Whether we should include timers from the structuretimers plugin in the calendar. Inherits view permissions from aa-structuretimers | True
OPCALENDAR_DISPLAY_MOONMINING | Whether we should include extractions from the aa-moonmining plugin in the calendar. Inherits view permissions from aa-moonmining | True
OPCALENDAR_DISCORD_OPS_DISPLAY_EXTERNAL | Whether we display external hosts such as ingame hosts in the discord ops command filters | False
OPCALENDAR_DISPLAY_MOONMINING_TAGS | Display the rarity tag of aa-moonmining moons if the moonmining plugin is installed | True
OPCALENDAR_DISPLAY_MOONMINING_ARRIVAL_TIME | Displays aa-moonmining extraction time based on arrival time. Set to False to display as auto fracture time | True
OPCALENDAR_NOTIFY_REPEAT_EVENTS | If repeated events should also be created as webhook pings on discord. Can create spam if the event repeat is set to high | True
OPCALENDAR_SHOW_EVENT_COUNTER | Shows a counter next to the opcalendar menu for events that the user has not responded to | True
OPCALENDAR_TASKS_TIME_LIMIT | Hard timeout for opcalendar's periodic tasks in seconds, to reduce task accumulation during outages | 7200
OPCALENDAR_DISCORD_BOT_TOKEN | Bot token for the self-hosted [Discord event channel bot](#discord-event-channel-bot). Leave blank to disable the feature entirely | "" (empty)

## Setup
Before you are able to create new events on the front end you will need to set up the needed hosts, categories and visibility filters for your events. All of the following are configured from the Django admin site.

### 1. Host
Hosts are for identifying reasons. If you run a single corporation or alliance entity you most likely only want one host. If you want to extend the calendar with other hosts such as NPSI communities you can create a host for each different entity.
- Host name is shown on the event and on discord notifications
- You can customize host logos
- Go to the admin site

### 2. Visibility filter
These filters will determine who is able to see the events that are labeled with each different visibility filter.
- Can be restricted to groups and states
- If no groups or states are selected the events will be visible for everyone
- You can determine a custom color tag that will be shown on the top right corner of the event
- Each visibility filter will be displayed on the calendar and can be used for filtering events on the calendar
- Discord notification webhooks can be assigned for each visibility filter. Events created, deleted or edited under this filter will then be sent over to discord.

### 3. Categories
Categories are displayed as a ticker in front of manually created events. Most common categories are: PvP, Stratop, Mining, CTA etc.
- Ticker displayed on event
- Custom colors
- Optional FontAwesome icon (e.g. `fas fa-rocket`), tinted with the category's color, shown in the calendar's left gutter. If no icon is set but a color is, a small colored dot is shown instead. Both blink for events marked "Important".

### 4. Discord webhook
If you want to receive notifications about your events (created/modified/deleted) on your discord you can add a webhook for the channel in discord you want to receive the notifications to. The webhooks you create will be used in the visibility filters.

## Adding manual events
To add a manual event simply go to the calendar page and press on the new event button. Fill in and select the needed information.

You can mark an event as **Important** to show a blinking red indicator on the calendar (and on its category icon/dot, if any) so members notice it at a glance. Ingame (ESI) calendar events that EVE itself flags as important get this automatically.

## Discord ping timing
When creating or editing an event you can choose when the Discord ping for it should be sent out. This is a multi select field, so you can pick multiple timings for the same event:

- **Now** - sends the ping as soon as the event is created
- **1 hour before fleet** - sends a reminder ping 1 hour before the event start time
- **1 day before fleet** - sends a reminder ping 1 day before the event start time
- **1 week before fleet** - sends a reminder ping 1 week before the event start time
- **Custom date and time** - sends a reminder ping at a specific date and time you set yourself

The "Now" ping is sent out instantly when the event is created. All other timings ("1 hour/day/week before fleet" and "Custom date and time") are picked up by a periodic task that checks for due notifications and sends them out.

### Sending a "starting now" ping
On the event details page, the owner of an event (the user who created it) will see a **Send starting now ping** button. This lets you instantly notify your Discord channel that the fleet is starting, regardless of which scheduled ping timings were selected when the event was created.
Next to the button you can choose who the ping should mention:
- **@here** - notifies only the members who are currently online in the Discord server
- **@everyone** - notifies all members of the Discord server

The notification is sent through the same webhook configured on the event's visibility filter, so it respects that filter's enabled/ignore-past-fleets settings.

## Importing NPSI fleets
Opcalendar has the ability to import predetermined NPSI fleets directly into your calendar from public NPSI community APIs.

This feature is optional and set up as part of the
[installation steps](#installation) (see "Optional: Importing NPSI fleets"
under either the Standard or Docker installation method).

> [!NOTE]
> Don't forget to also set up the `send_scheduled_event_notifications` periodic task described in [Setup &mdash; Periodic task for scheduled Discord pings](#5-periodic-task-for-scheduled-discord-pings) — it's required for the Discord ping timing feature and is unrelated to NPSI imports.

### Supported NPSI communities
Opcalendar is currently supporting imports for the following NPSI fleets:

- EVE LinkNet
- Spectre Fleet
- EVE University (classes)
- Fun Inc.
- FRIDAY YARRRR
- Redemption Road
- CAS
- Fwaming Dwagons
- FREE RANGE CHIKUNS

## Importing fleets from ingame calendar
You can import events that have been created in the ingame calendar. As the fields on the ingame calendar are limited the events will not be as detailed as when created directly from the calendar.

This feature is optional and set up as part of the
[installation steps](#installation) (see "Optional: Importing fleets from
ingame calendar" under either the Standard or Docker installation method).

### Ingame event visibility and categories
By default, the ingame events you import have no visibility filter and no category. This means they **will be visible for everyone**.

If you wish to add a visibility filter or a category similar to the manual events, go to **admin panel → Ingame event owners** and select a filter and a category for the owner.

After selecting a visibility filter and a category, the ingame events will behave similarly to manual events and respect the group and state restrictions set for the visibility filters. Ingame events EVE itself flags as important will also show the "Important" blinking indicator automatically.

## Discord event channel bot
Opcalendar can run a self-hosted Discord bot that posts and keeps a single pinned summary embed of upcoming events (both manually created events and ingame ESI events) in sync inside a Discord channel, and lets members sign up for events directly from Discord using a slash command.

This feature is optional and set up as part of the
[installation steps](#installation) (see "Optional: Discord event channel
bot" under either the Standard or Docker installation method).

### Signing up from Discord
Members can sign up for an event listed in the pinned summary using the `/signup <number> [attending|maybe|declined]` slash command from within the event channel, where `<number>` is the position of the event as listed in the pinned summary.

## Contributing
Make sure you have signed the [License Agreement](https://developers.eveonline.com/resource/license-agreement) by logging in at https://developers.eveonline.com before submitting any pull requests. All bug fixes or features must not include extra superfluous formatting changes.
