Compare commits
No commits in common. "master" and "3.3.0" have entirely different histories.
36
.drone.yml
Normal file
36
.drone.yml
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
kind: pipeline
|
||||||
|
name: default
|
||||||
|
type: docker
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: lint
|
||||||
|
image: python:3.8
|
||||||
|
commands:
|
||||||
|
- pip install poetry
|
||||||
|
- poetry config installer.modern-installation false
|
||||||
|
- poetry install
|
||||||
|
- poetry run flake8 .
|
||||||
|
- poetry run mypy .
|
||||||
|
- poetry run djlint .
|
||||||
|
|
||||||
|
- name: docker
|
||||||
|
image: plugins/docker
|
||||||
|
settings:
|
||||||
|
repo: xefir/divent
|
||||||
|
auto_tag: true
|
||||||
|
username:
|
||||||
|
from_secret: docker_username
|
||||||
|
password:
|
||||||
|
from_secret: docker_password
|
||||||
|
|
||||||
|
- name: publish
|
||||||
|
image: python:3.8
|
||||||
|
commands:
|
||||||
|
- pip install poetry
|
||||||
|
- poetry config installer.modern-installation false
|
||||||
|
- poetry publish --build
|
||||||
|
environment:
|
||||||
|
POETRY_PYPI_TOKEN_PYPI:
|
||||||
|
from_secret: pypi_token
|
||||||
|
when:
|
||||||
|
event: tag
|
@ -1,56 +0,0 @@
|
|||||||
name: divent
|
|
||||||
on: [push]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
lint:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container: python:3.12.6
|
|
||||||
steps:
|
|
||||||
- run: apt-get update
|
|
||||||
- run: apt-get install -y git nodejs
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: Gr1N/setup-poetry@v9
|
|
||||||
- run: poetry install
|
|
||||||
- run: poetry run ruff check .
|
|
||||||
- run: poetry run mypy .
|
|
||||||
- run: poetry run djlint .
|
|
||||||
|
|
||||||
docker:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container: docker
|
|
||||||
needs: [lint]
|
|
||||||
steps:
|
|
||||||
- run: apk add git nodejs
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: docker/metadata-action@v5
|
|
||||||
id: meta
|
|
||||||
with:
|
|
||||||
images: xefir/divent
|
|
||||||
tags: |
|
|
||||||
type=schedule
|
|
||||||
type=ref,event=tag
|
|
||||||
type=ref,event=pr
|
|
||||||
type=raw,value=latest,enable={{is_default_branch}}
|
|
||||||
- uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
- uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
push: ${{ gitea.ref_name == gitea.event.repository.default_branch || gitea.ref_type == 'tag' }}
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
|
|
||||||
pypi:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container: python:3.12.6
|
|
||||||
needs: [lint]
|
|
||||||
if: gitea.ref_type == 'tag'
|
|
||||||
env:
|
|
||||||
POETRY_PYPI_TOKEN_PYPI: ${{ secrets.POETRY_PYPI_TOKEN_PYPI }}
|
|
||||||
steps:
|
|
||||||
- run: apt-get update
|
|
||||||
- run: apt-get install -y git nodejs
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: Gr1N/setup-poetry@v9
|
|
||||||
- run: poetry publish --build
|
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -107,10 +107,8 @@ ipython_config.py
|
|||||||
#pdm.lock
|
#pdm.lock
|
||||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
# in version control.
|
# in version control.
|
||||||
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
# https://pdm.fming.dev/#use-with-ide
|
||||||
.pdm.toml
|
.pdm.toml
|
||||||
.pdm-python
|
|
||||||
.pdm-build/
|
|
||||||
|
|
||||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
__pypackages__/
|
__pypackages__/
|
||||||
@ -160,4 +158,4 @@ cython_debug/
|
|||||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
#.idea/
|
.idea/
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
FROM python:3.12.6 as build
|
FROM python:3.11.2 as build
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN pip install poetry && poetry build
|
RUN pip install poetry && poetry build
|
||||||
|
|
||||||
FROM python:3.12.6
|
FROM python:3.11.2
|
||||||
|
|
||||||
COPY --from=build /app/dist /tmp/dist
|
COPY --from=build /app/dist /tmp/dist
|
||||||
RUN pip install /tmp/dist/*.whl && rm -rf /tmp/dist
|
RUN pip install /tmp/dist/*.whl && rm -rf /tmp/dist
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# Divent
|
# Divent
|
||||||
> The discord scheduled event calendar generator
|
> The discord scheduled event calendar generator
|
||||||
|
|
||||||
|
[![Build Status](https://ci.crystalyx.net/api/badges/Xefir/Divent/status.svg)](https://ci.crystalyx.net/Xefir/Divent)
|
||||||
[![Docker Hub](https://img.shields.io/docker/pulls/xefir/divent)](https://hub.docker.com/r/xefir/divent)
|
[![Docker Hub](https://img.shields.io/docker/pulls/xefir/divent)](https://hub.docker.com/r/xefir/divent)
|
||||||
|
|
||||||
Simple website that guides you to invite a bot to read and format scheduled events to a subscribable calendar.
|
Simple website that guides you to invite a bot to read and format scheduled events to a subscribable calendar.
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
from divent.bot import run
|
|
||||||
|
|
||||||
run()
|
|
240
divent/bot.py
240
divent/bot.py
@ -3,17 +3,17 @@ import logging
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from os import getenv, path
|
from os import getenv, path
|
||||||
from typing import Dict, Optional, Union
|
from typing import Dict, Optional
|
||||||
|
|
||||||
from disnake import Asset, Client, Guild, Intents, Member, User
|
from disnake import Client, Guild
|
||||||
from disnake.guild_scheduled_event import GuildScheduledEvent
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from hypercorn.middleware import ProxyFixMiddleware
|
|
||||||
from ics import Calendar, ContentLine, Event
|
from ics import Calendar, ContentLine, Event
|
||||||
from ics.alarm import DisplayAlarm
|
from ics.alarm import DisplayAlarm
|
||||||
from oauthlib.oauth2 import OAuth2Error
|
from oauthlib.oauth2 import TokenExpiredError
|
||||||
from quart import Quart, redirect, render_template, request, session, url_for
|
from quart import Quart, redirect, render_template, request, session, url_for
|
||||||
from requests_oauthlib import OAuth2Session # type: ignore
|
from requests_oauthlib import OAuth2Session # type: ignore
|
||||||
|
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware # type: ignore
|
||||||
|
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
@ -35,51 +35,16 @@ API_BASE_URL = getenv("API_BASE_URL", "https://discordapp.com/api")
|
|||||||
AUTHORIZATION_BASE_URL = f"{API_BASE_URL}/oauth2/authorize"
|
AUTHORIZATION_BASE_URL = f"{API_BASE_URL}/oauth2/authorize"
|
||||||
TOKEN_URL = f"{API_BASE_URL}/oauth2/token"
|
TOKEN_URL = f"{API_BASE_URL}/oauth2/token"
|
||||||
|
|
||||||
CATALOG_CACHE = {}
|
|
||||||
EVENTS_CACHE = {}
|
|
||||||
|
|
||||||
|
|
||||||
class Discord(Client):
|
class Discord(Client):
|
||||||
async def on_ready(self):
|
async def on_ready(self):
|
||||||
print(f"Logged on as {self.user}!", flush=True)
|
print(f"Logged on as {self.user}!", flush=True)
|
||||||
|
|
||||||
for guild in self.guilds:
|
|
||||||
for scheduled_event in guild.scheduled_events:
|
|
||||||
EVENTS_CACHE[scheduled_event.id] = [
|
|
||||||
member.id
|
|
||||||
for member in await scheduled_event.fetch_users().flatten()
|
|
||||||
]
|
|
||||||
|
|
||||||
print("Events synchronised!", flush=True)
|
|
||||||
|
|
||||||
async def on_guild_scheduled_event_subscribe(
|
|
||||||
self, event: GuildScheduledEvent, user: Union[Member, User]
|
|
||||||
):
|
|
||||||
EVENTS_CACHE[event.id].append(user.id)
|
|
||||||
|
|
||||||
async def on_guild_scheduled_event_unsubscribe(
|
|
||||||
self, event: GuildScheduledEvent, user: Union[Member, User]
|
|
||||||
):
|
|
||||||
EVENTS_CACHE[event.id].remove(user.id)
|
|
||||||
|
|
||||||
async def on_guild_scheduled_event_create(self, event: GuildScheduledEvent):
|
|
||||||
EVENTS_CACHE[event.id] = [
|
|
||||||
member.id for member in await event.fetch_users().flatten()
|
|
||||||
]
|
|
||||||
|
|
||||||
async def on_guild_scheduled_event_delete(self, event: GuildScheduledEvent):
|
|
||||||
EVENTS_CACHE.pop(event.id)
|
|
||||||
|
|
||||||
|
|
||||||
intents = Intents.default()
|
|
||||||
intents.guild_scheduled_events = True
|
|
||||||
intents.members = True
|
|
||||||
client = Discord(intents=intents)
|
|
||||||
|
|
||||||
|
client = Discord()
|
||||||
app = Quart(__name__)
|
app = Quart(__name__)
|
||||||
app.config["SECRET_KEY"] = OAUTH2_CLIENT_SECRET
|
app.config["SECRET_KEY"] = OAUTH2_CLIENT_SECRET
|
||||||
app.config["EXPLAIN_TEMPLATE_LOADING"] = QUART_DEBUG
|
app.asgi_app = ProxyHeadersMiddleware(app.asgi_app, "*") # type: ignore
|
||||||
app.asgi_app = ProxyFixMiddleware(app.asgi_app) # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def get_guild_by_id(guild_id: str) -> Optional[Guild]:
|
def get_guild_by_id(guild_id: str) -> Optional[Guild]:
|
||||||
@ -90,6 +55,9 @@ def get_guild_by_id(guild_id: str) -> Optional[Guild]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
CATALOG_CACHE = {}
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(500)
|
@app.errorhandler(500)
|
||||||
async def errorhandler(error: Exception):
|
async def errorhandler(error: Exception):
|
||||||
print(f"\33[31m{error}\33[m", flush=True)
|
print(f"\33[31m{error}\33[m", flush=True)
|
||||||
@ -147,11 +115,17 @@ def days_before_failure() -> int:
|
|||||||
return nextDelta.days
|
return nextDelta.days
|
||||||
|
|
||||||
|
|
||||||
|
def cdn_avatar_url(user_id: int, hash: str) -> str:
|
||||||
|
ext = "gif" if hash.startswith("a_") else "png"
|
||||||
|
return f"https://cdn.discordapp.com/avatars/{user_id}/{hash}.{ext}"
|
||||||
|
|
||||||
|
|
||||||
@app.context_processor
|
@app.context_processor
|
||||||
def context_processor():
|
def context_processor():
|
||||||
return dict(
|
return dict(
|
||||||
_=i18n,
|
_=i18n,
|
||||||
client=client,
|
client=client,
|
||||||
|
cdn_avatar_url=cdn_avatar_url,
|
||||||
days_before_failure=days_before_failure(),
|
days_before_failure=days_before_failure(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -187,33 +161,31 @@ async def callback():
|
|||||||
if request_values.get("error"):
|
if request_values.get("error"):
|
||||||
return errorhandler(request_values.get("error"))
|
return errorhandler(request_values.get("error"))
|
||||||
|
|
||||||
try:
|
discord = make_session(state=session.get("oauth2_state"))
|
||||||
discord = make_session(state=session.get("oauth2_state"))
|
token = discord.fetch_token(
|
||||||
token = discord.fetch_token(
|
TOKEN_URL,
|
||||||
TOKEN_URL,
|
client_secret=OAUTH2_CLIENT_SECRET,
|
||||||
client_secret=OAUTH2_CLIENT_SECRET,
|
authorization_response=request.url,
|
||||||
authorization_response=request.url,
|
)
|
||||||
)
|
token_updater(token)
|
||||||
token_updater(token)
|
|
||||||
except OAuth2Error as e:
|
|
||||||
return errorhandler(e)
|
|
||||||
|
|
||||||
return redirect(session.pop("redirect_url", url_for(".guilds")))
|
return redirect(session.pop("redirect_url", url_for(".guilds")))
|
||||||
|
|
||||||
|
|
||||||
@app.route("/guilds")
|
@app.route("/guilds")
|
||||||
@login_required
|
@login_required
|
||||||
async def guilds():
|
async def guilds():
|
||||||
guild = request.args.get("guild")
|
guild = get_guild_by_id(request.args.get("guild"))
|
||||||
|
|
||||||
if guild:
|
if guild:
|
||||||
return redirect(url_for(".subscribe", entity_id=guild))
|
return redirect(
|
||||||
|
url_for(".subscribe", guild_id=guild.vanity_url_code or guild.id)
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
discord = make_session(token=session.get("oauth2_token"))
|
discord = make_session(token=session.get("oauth2_token"))
|
||||||
user = discord.get(f"{API_BASE_URL}/users/@me").json()
|
user = discord.get(f"{API_BASE_URL}/users/@me").json()
|
||||||
user_guilds = discord.get(f"{API_BASE_URL}/users/@me/guilds").json()
|
user_guilds = discord.get(f"{API_BASE_URL}/users/@me/guilds").json()
|
||||||
except OAuth2Error:
|
except TokenExpiredError:
|
||||||
return redirect(url_for(".login"))
|
return redirect(url_for(".login"))
|
||||||
|
|
||||||
common_guilds = []
|
common_guilds = []
|
||||||
@ -223,124 +195,72 @@ async def guilds():
|
|||||||
common_guilds.append(bot_guild)
|
common_guilds.append(bot_guild)
|
||||||
|
|
||||||
return await render_template(
|
return await render_template(
|
||||||
"guilds.html.j2",
|
"guilds.html.j2", user=user, common_guilds=common_guilds
|
||||||
user=user,
|
|
||||||
avatar=Asset._from_avatar(None, user["id"], user["avatar"]),
|
|
||||||
common_guilds=common_guilds,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/subscribe/<entity_id>")
|
@app.route("/subscribe/<guild_id>")
|
||||||
@login_required
|
@login_required
|
||||||
async def subscribe(entity_id: str):
|
async def subscribe(guild_id: str):
|
||||||
guild = get_guild_by_id(entity_id)
|
guild = get_guild_by_id(guild_id)
|
||||||
|
if guild is None:
|
||||||
if guild:
|
|
||||||
try:
|
|
||||||
discord = make_session(token=session.get("oauth2_token"))
|
|
||||||
user_guilds = discord.get(f"{API_BASE_URL}/users/@me/guilds").json()
|
|
||||||
except OAuth2Error:
|
|
||||||
return redirect(url_for(".login"))
|
|
||||||
|
|
||||||
if not any(str(guild.id) == user_guild["id"] for user_guild in user_guilds):
|
|
||||||
return redirect(url_for(".login"))
|
|
||||||
|
|
||||||
return await render_template(
|
|
||||||
"subscribe.html.j2",
|
|
||||||
avatar=guild.icon,
|
|
||||||
entity_id=guild.vanity_url_code or guild.id,
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
user = await client.get_or_fetch_user(int(entity_id))
|
|
||||||
except ValueError:
|
|
||||||
return redirect(url_for(".login"))
|
return redirect(url_for(".login"))
|
||||||
|
|
||||||
if user and str(user.id) == entity_id:
|
|
||||||
return await render_template(
|
|
||||||
"subscribe.html.j2", avatar=user.avatar, entity_id=user.id
|
|
||||||
)
|
|
||||||
|
|
||||||
return redirect(url_for(".login"))
|
|
||||||
|
|
||||||
|
|
||||||
def make_event(scheduled_event: GuildScheduledEvent) -> Event:
|
|
||||||
event = Event()
|
|
||||||
|
|
||||||
event.summary = scheduled_event.name
|
|
||||||
event.begin = scheduled_event.scheduled_start_time
|
|
||||||
event.end = scheduled_event.scheduled_end_time
|
|
||||||
event.duration = timedelta(hours=2)
|
|
||||||
event.uid = str(scheduled_event.id)
|
|
||||||
event.description = scheduled_event.description
|
|
||||||
event.url = scheduled_event.url
|
|
||||||
event.location = (
|
|
||||||
scheduled_event.entity_metadata.location
|
|
||||||
if scheduled_event.entity_metadata
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
|
|
||||||
alarm = DisplayAlarm()
|
|
||||||
alarm.trigger = timedelta(hours=-1)
|
|
||||||
event.alarms.append(alarm)
|
|
||||||
|
|
||||||
return event
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/<entity_id>.ics")
|
|
||||||
async def ical(entity_id: str):
|
|
||||||
guild = get_guild_by_id(entity_id)
|
|
||||||
|
|
||||||
if guild:
|
|
||||||
calendar = Calendar()
|
|
||||||
|
|
||||||
calendar.extra.append(ContentLine(name="REFRESH-INTERVAL", value="PT1H"))
|
|
||||||
calendar.extra.append(ContentLine(name="X-PUBLISHED-TTL", value="PT1H"))
|
|
||||||
|
|
||||||
calendar.extra.append(ContentLine(name="NAME", value=guild.name))
|
|
||||||
calendar.extra.append(ContentLine(name="X-WR-CALNAME", value=guild.name))
|
|
||||||
|
|
||||||
if guild.description:
|
|
||||||
calendar.extra.append(
|
|
||||||
ContentLine(name="DESCRIPTION", value=guild.description)
|
|
||||||
)
|
|
||||||
calendar.extra.append(
|
|
||||||
ContentLine(name="X-WR-CALDESC", value=guild.description)
|
|
||||||
)
|
|
||||||
|
|
||||||
for scheduled_event in guild.scheduled_events:
|
|
||||||
event = make_event(scheduled_event)
|
|
||||||
calendar.events.append(event)
|
|
||||||
|
|
||||||
return calendar.serialize()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user = await client.get_or_fetch_user(int(entity_id))
|
discord = make_session(token=session.get("oauth2_token"))
|
||||||
except ValueError:
|
user_guilds = discord.get(f"{API_BASE_URL}/users/@me/guilds").json()
|
||||||
|
except TokenExpiredError:
|
||||||
return redirect(url_for(".login"))
|
return redirect(url_for(".login"))
|
||||||
|
|
||||||
if user:
|
if not any(str(guild.id) == user_guild["id"] for user_guild in user_guilds):
|
||||||
calendar = Calendar()
|
return redirect(url_for(".login"))
|
||||||
|
|
||||||
calendar.extra.append(ContentLine(name="REFRESH-INTERVAL", value="PT1H"))
|
return await render_template("subscribe.html.j2", guild=guild)
|
||||||
calendar.extra.append(ContentLine(name="X-PUBLISHED-TTL", value="PT1H"))
|
|
||||||
|
|
||||||
calendar.extra.append(ContentLine(name="NAME", value=client.user.display_name))
|
|
||||||
calendar.extra.append(
|
@app.route("/<guild_id>.ics")
|
||||||
ContentLine(name="X-WR-CALNAME", value=client.user.display_name)
|
async def ical(guild_id: str):
|
||||||
|
guild = get_guild_by_id(guild_id)
|
||||||
|
if guild is None:
|
||||||
|
return redirect(url_for(".login"))
|
||||||
|
|
||||||
|
calendar = Calendar()
|
||||||
|
|
||||||
|
calendar.extra.append(ContentLine(name="REFRESH-INTERVAL", value="PT1H"))
|
||||||
|
calendar.extra.append(ContentLine(name="X-PUBLISHED-TTL", value="PT1H"))
|
||||||
|
|
||||||
|
calendar.extra.append(ContentLine(name="NAME", value=guild.name))
|
||||||
|
calendar.extra.append(ContentLine(name="X-WR-CALNAME", value=guild.name))
|
||||||
|
|
||||||
|
if guild.description:
|
||||||
|
calendar.extra.append(ContentLine(name="DESCRIPTION", value=guild.description))
|
||||||
|
calendar.extra.append(ContentLine(name="X-WR-CALDESC", value=guild.description))
|
||||||
|
|
||||||
|
for scheduled_event in guild.scheduled_events:
|
||||||
|
event = Event()
|
||||||
|
event.summary = scheduled_event.name
|
||||||
|
event.begin = scheduled_event.scheduled_start_time
|
||||||
|
event.end = scheduled_event.scheduled_end_time
|
||||||
|
event.duration = timedelta(hours=2)
|
||||||
|
event.uid = str(scheduled_event.id)
|
||||||
|
event.description = scheduled_event.description
|
||||||
|
event.url = f"https://discord.com/events/{guild_id}/{scheduled_event.id}"
|
||||||
|
event.location = (
|
||||||
|
scheduled_event.entity_metadata.location
|
||||||
|
if scheduled_event.entity_metadata
|
||||||
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
for guild in client.guilds:
|
alarm = DisplayAlarm()
|
||||||
for scheduled_event in guild.scheduled_events:
|
alarm.trigger = timedelta(hours=-1)
|
||||||
if user.id in EVENTS_CACHE[scheduled_event.id]:
|
event.alarms.append(alarm)
|
||||||
event = make_event(scheduled_event)
|
|
||||||
calendar.events.append(event)
|
|
||||||
|
|
||||||
return calendar.serialize()
|
calendar.events.append(event)
|
||||||
|
|
||||||
return redirect(url_for(".login"))
|
return calendar.serialize()
|
||||||
|
|
||||||
|
|
||||||
def run():
|
def __main__():
|
||||||
client.loop.create_task(client.start(DISCORD_TOKEN))
|
quart_task = client.loop.create_task(app.run_task("0.0.0.0"))
|
||||||
app.run("0.0.0.0", loop=client.loop)
|
quart_task.add_done_callback(lambda f: client.loop.stop())
|
||||||
|
client.run(DISCORD_TOKEN)
|
||||||
|
@ -6,11 +6,11 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="description" content="" />
|
<meta name="description" content="" />
|
||||||
<meta name="keywords" content="" />
|
<meta name="keywords" content="" />
|
||||||
<title>{{ client.user.display_name }} - {{ _("The discord scheduled event calendar generator") }}</title>
|
<title>{{ client.user.display_name }} - {{ _('The discord scheduled event calendar generator') }}</title>
|
||||||
<link rel="stylesheet"
|
<link rel="stylesheet"
|
||||||
href="{{ url_for('static', filename='css/font-awesome.min.css') }}" />
|
href="{{ url_for('static', filename='css/font-awesome.min.css') }}"/>
|
||||||
<link rel="stylesheet"
|
<link rel="stylesheet"
|
||||||
href="{{ url_for('static', filename='css/global.css') }}" />
|
href="{{ url_for('static', filename='css/global.css') }}"/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="content">
|
<div id="content">
|
||||||
|
@ -3,17 +3,17 @@
|
|||||||
<div id="box">
|
<div id="box">
|
||||||
<div id="avatars">
|
<div id="avatars">
|
||||||
<img src="{{ url_for('static', filename='img/deadlink.png') }}"
|
<img src="{{ url_for('static', filename='img/deadlink.png') }}"
|
||||||
alt="{{ _("Link is dead") }}"
|
alt="{{ _('Link is dead') }}"
|
||||||
height="179"
|
height="179"
|
||||||
width="173" />
|
width="173"/>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<span>{{ error }}</span>
|
<span>{{ error }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="buttons">
|
<div id="buttons">
|
||||||
<a href="{{ url_for("index") }}">
|
<a href="{{ url_for('index') }}">
|
||||||
<i class="fa fa-arrow-left"></i>
|
<i class="fa fa-arrow-left"></i>
|
||||||
{{ _("Back to the beginning") }}
|
{{ _('Back to the beginning') }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<li>
|
<li>
|
||||||
<a href="https://discord.com/users/133305654512320513" target="_blank">
|
<a href="https://discord.com/users/133305654512320513" target="_blank">
|
||||||
<i class="fa fa-user-plus"></i>
|
<i class="fa fa-user-plus"></i>
|
||||||
{{ _("Add author on Discord") }}
|
{{ _('Add author on Discord') }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
@ -16,17 +16,17 @@
|
|||||||
<li>
|
<li>
|
||||||
<a href="https://git.crystalyx.net/Xefir/Divent" target="_blank">
|
<a href="https://git.crystalyx.net/Xefir/Divent" target="_blank">
|
||||||
<i class="fa fa-code-fork"></i>
|
<i class="fa fa-code-fork"></i>
|
||||||
{{ _("View the source code") }}
|
{{ _('View the source code') }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://hub.docker.com/r/xefir/divent" target="_blank">
|
<a href="https://hub.docker.com/r/xefir/divent" target="_blank">
|
||||||
<i class="fa fa-cubes"></i>
|
<i class="fa fa-cubes"></i>
|
||||||
{{ _("Host it yourself") }}
|
{{ _('Host it yourself') }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<i class="fa fa-heartbeat"></i>
|
<i class="fa fa-heartbeat"></i>
|
||||||
{{ _("Next castastrophic life failure in about %days% days") | replace('%days%', days_before_failure) }}
|
{{ _('Next castastrophic life failure in about %days% days') | replace('%days%', days_before_failure) }}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -4,30 +4,32 @@
|
|||||||
<div id="box">
|
<div id="box">
|
||||||
<div id="avatars">
|
<div id="avatars">
|
||||||
<img src="{{ client.user.display_avatar }}"
|
<img src="{{ client.user.display_avatar }}"
|
||||||
alt="{{ _("Bot Logo") }}"
|
alt="{{ _('Bot Logo') }}"
|
||||||
width="80"
|
width="80"
|
||||||
height="80" />
|
height="80"/>
|
||||||
<span id="dots">…</span>
|
<span id="dots">…</span>
|
||||||
<img src="{{ avatar.url }}"
|
<img src="{{ cdn_avatar_url(user.id, user.avatar) }}"
|
||||||
alt="{{ _("User Avatar") }}"
|
alt="{{ _('User Avatar') }}"
|
||||||
width="80"
|
width="80"
|
||||||
height="80" />
|
height="80"/>
|
||||||
</div>
|
</div>
|
||||||
<h1>
|
<h1>
|
||||||
<a href="{{ url_for(".index") }}">{{ client.user.display_name }}</a>
|
<a href="{{ url_for(".index") }}">{{ client.user.display_name }}</a>
|
||||||
</h1>
|
</h1>
|
||||||
<h2>{{ _("The discord scheduled event calendar generator") }}</h2>
|
<h3>{{ _('Choose a server:') }}</h3>
|
||||||
<hr>
|
<select name="guild" class="black_input">
|
||||||
<a class="button" href="{{ url_for(".guilds", guild=user.id) }}">{{ _("For all your servers") }}</a>
|
<option>
|
||||||
<div class="hr-sect">{{ _("OR") }}</div>
|
|
||||||
<select name="guild" class="black_input" onchange="this.form.submit()">
|
</option>
|
||||||
<option> </option>
|
|
||||||
{% for guild in common_guilds %}
|
{% for guild in common_guilds %}
|
||||||
<option value="{{ guild.vanity_url_code|default(guild.id, True) }}">{{ guild.name }}</option>
|
<option value="{{ guild.vanity_url_code|default(guild.id, True) }}">
|
||||||
|
{{ guild.name }}
|
||||||
|
</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
<div class="hr-sect">{{ _("OR") }}</div>
|
<div class="hr-sect">{{ _("OR") }}</div>
|
||||||
<a class="button"
|
<a class="button"
|
||||||
|
target="_blank"
|
||||||
href="https://discord.com/api/oauth2/authorize?client_id={{ client.user.id }}&permissions=8589934592&scope=bot">
|
href="https://discord.com/api/oauth2/authorize?client_id={{ client.user.id }}&permissions=8589934592&scope=bot">
|
||||||
{{ _("Add the bot on your server") }}
|
{{ _("Add the bot on your server") }}
|
||||||
</a>
|
</a>
|
||||||
@ -46,5 +48,8 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="buttons">
|
||||||
|
<input type="submit" class="button" value="{{ _("Let's go!") }}"/>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
@ -4,24 +4,24 @@
|
|||||||
<div id="box">
|
<div id="box">
|
||||||
<div id="avatars">
|
<div id="avatars">
|
||||||
<img src="{{ client.user.display_avatar }}"
|
<img src="{{ client.user.display_avatar }}"
|
||||||
alt="{{ _("Bot Logo") }}"
|
alt="{{ _('Bot Logo') }}"
|
||||||
width="80"
|
width="80"
|
||||||
height="80" />
|
height="80"/>
|
||||||
</div>
|
</div>
|
||||||
<h1>
|
<h1>
|
||||||
<a href="{{ url_for(".index") }}">{{ client.user.display_name }}</a>
|
<a href="{{ url_for(".index") }}">{{ client.user.display_name }}</a>
|
||||||
</h1>
|
</h1>
|
||||||
<h2>{{ _("The discord scheduled event calendar generator") }}</h2>
|
<h2>{{ _('The discord scheduled event calendar generator') }}</h2>
|
||||||
<hr />
|
<hr />
|
||||||
<h3>{{ _("This will allow you to:") }}</h3>
|
<h3>{{ _('This will allow you to:') }}</h3>
|
||||||
<ul id="scopes">
|
<ul id="scopes">
|
||||||
<li>
|
<li>
|
||||||
<i class="fa fa-custom-circle fa-check"></i>
|
<i class="fa fa-custom-circle fa-check"></i>
|
||||||
{{ _("Subscribe to a calendar on Google, Outlook, Apple or any ICS complient software") }}
|
{{ _('Subscribe to a calendar on Google, Outlook, Apple or any ICS complient software') }}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<i class="fa fa-custom-circle fa-times"></i>
|
<i class="fa fa-custom-circle fa-times"></i>
|
||||||
{{ _("Throwing you to a new isekai world") }}
|
{{ _('Throwing you to a new isekai world') }}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,22 +3,25 @@
|
|||||||
<div id="box">
|
<div id="box">
|
||||||
<div id="avatars">
|
<div id="avatars">
|
||||||
<img src="{{ client.user.display_avatar }}"
|
<img src="{{ client.user.display_avatar }}"
|
||||||
alt="{{ _("Bot Logo") }}"
|
alt="{{ _('Bot Logo') }}"
|
||||||
width="80"
|
width="80"
|
||||||
height="80" />
|
height="80"/>
|
||||||
<span id="dots">…</span>
|
<span id="dots">…</span>
|
||||||
<img src="{{ avatar.url }}" alt="{{ _("Avatar") }}" width="80" height="80" />
|
<img src="{{ guild.icon.url }}"
|
||||||
|
alt="{{ _('Guild Logo') }}"
|
||||||
|
width="80"
|
||||||
|
height="80"/>
|
||||||
</div>
|
</div>
|
||||||
<h1>
|
<h1>
|
||||||
<a href="{{ url_for(".index") }}">{{ client.user.display_name }}</a>
|
<a href="{{ url_for(".index") }}">{{ client.user.display_name }}</a>
|
||||||
</h1>
|
</h1>
|
||||||
<h2>{{ _("The discord scheduled event calendar generator") }}</h2>
|
<h2>{{ _('The discord scheduled event calendar generator') }}</h2>
|
||||||
<hr />
|
<hr />
|
||||||
<ul id="providers">
|
<ul id="providers">
|
||||||
<li>
|
<li>
|
||||||
<a class="button"
|
<a class="button"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href="https://calendar.google.com/calendar/u/0/r?cid=webcal://{{ request.host }}/{{ entity_id }}.ics">
|
href="https://calendar.google.com/calendar/u/0/r?cid=webcal://{{ request.host }}/{{ guild.vanity_url_code|default(guild.id, True) }}.ics">
|
||||||
<i class="fa fa-google"></i>
|
<i class="fa fa-google"></i>
|
||||||
{{ _("Subscribe to") }} Google
|
{{ _("Subscribe to") }} Google
|
||||||
</a>
|
</a>
|
||||||
@ -26,14 +29,14 @@
|
|||||||
<li>
|
<li>
|
||||||
<a class="button"
|
<a class="button"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href="https://outlook.live.com/owa?path=/calendar/action/compose&rru=addsubscription&url=webcal://{{ request.host }}/{{ entity_id }}.ics">
|
href="https://outlook.live.com/owa?path=/calendar/action/compose&rru=addsubscription&url=webcal://{{ request.host }}/{{ guild.vanity_url_code|default(guild.id, True) }}.ics">
|
||||||
<i class="fa fa-windows"></i>
|
<i class="fa fa-windows"></i>
|
||||||
{{ _("Subscribe to") }} Outlook
|
{{ _("Subscribe to") }} Outlook
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
{# djlint:off #}
|
{# djlint:off #}
|
||||||
<a class="button" target="_blank" href="webcal://{{ request.host }}/{{ entity_id }}.ics">
|
<a class="button" target="_blank" href="webcal://{{ request.host }}/{{ guild.vanity_url_code|default(guild.id, True) }}.ics">
|
||||||
<i class="fa fa-apple"></i>
|
<i class="fa fa-apple"></i>
|
||||||
{{ _("Subscribe to") }} Apple
|
{{ _("Subscribe to") }} Apple
|
||||||
</a>
|
</a>
|
||||||
@ -44,7 +47,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<h3>{{ _("Use the direct link:") }}</h3>
|
<h3>{{ _("Use the direct link:") }}</h3>
|
||||||
{# djlint:off #}
|
{# djlint:off #}
|
||||||
<input type="text" readonly class="black_input" value="webcal://{{ request.host }}/{{ entity_id }}.ics"/>
|
<input type="text" readonly class="black_input" value="webcal://{{ request.host }}/{{ guild.vanity_url_code|default(guild.id, True) }}.ics"/>
|
||||||
{# djlint:on #}
|
{# djlint:on #}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
"This will allow you to:": "Ceci te permettra de :",
|
"This will allow you to:": "Ceci te permettra de :",
|
||||||
"Subscribe to a calendar on Google, Outlook, Apple or any ICS complient software": "T'abonner à un calendrier sur Google, Outlook, Apple ou tout autre logiciel compatible",
|
"Subscribe to a calendar on Google, Outlook, Apple or any ICS complient software": "T'abonner à un calendrier sur Google, Outlook, Apple ou tout autre logiciel compatible",
|
||||||
"Throwing you to a new isekai world": "T'envoyer dans un monde fantaisiste armée d'une poêle à frire",
|
"Throwing you to a new isekai world": "T'envoyer dans un monde fantaisiste armée d'une poêle à frire",
|
||||||
"For all your servers": "Pour tous tes serveurs",
|
"Choose a server:": "Choisi un serveur :",
|
||||||
"You must have": "Tu dois avoir la permission",
|
"You must have": "Tu dois avoir la permission",
|
||||||
"Manage Server": "Gérer le serveur",
|
"Manage Server": "Gérer le serveur",
|
||||||
"permission on this server to perform this action": "sur ce serveur pour effectuer cette action",
|
"permission on this server to perform this action": "sur ce serveur pour effectuer cette action",
|
||||||
|
2014
poetry.lock
generated
2014
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,37 +1,38 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "divent"
|
name = "divent"
|
||||||
version = "4.1.4"
|
version = "3.3.0"
|
||||||
description = "The discord scheduled event calendar generator"
|
description = "The discord scheduled event calendar generator"
|
||||||
authors = ["Xéfir Destiny <xefir@crystalyx.net>"]
|
authors = ["Xéfir Destiny"]
|
||||||
license = "WTFPL"
|
license = "WTFPL"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
homepage = "https://divent.crystalyx.net/"
|
homepage = "https://divent.crystalyx.net/"
|
||||||
repository = "https://git.crystalyx.net/Xefir/Divent"
|
repository = "https://git.crystalyx.net/Xefir/Divent"
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
divent = 'divent.bot:run'
|
divent = 'divent.bot:__main__'
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = ">=3.8,<4.0"
|
python = "^3.8.1"
|
||||||
disnake = "^2.9.2"
|
disnake = "^2.8.1"
|
||||||
ics = "0.8.0.dev0"
|
ics = "0.8.0.dev0"
|
||||||
python-dotenv = "^1.0.1"
|
python-dotenv = "^1.0.0"
|
||||||
quart = "^0.19.6"
|
quart = "^0.18.3"
|
||||||
requests-oauthlib = "^2.0.0"
|
requests-oauthlib = "^1.3.1"
|
||||||
|
uvicorn = "^0.21.1"
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
djlint = "^1.35.2"
|
black = "^23.3.0"
|
||||||
mypy = "^1.11.2"
|
djlint = "^1.19.16"
|
||||||
ruff = "^0.6.5"
|
flake8 = "^6.0.0"
|
||||||
types-oauthlib = "^3.2.0"
|
flake8-alphabetize = "^0.0.20"
|
||||||
|
flake8-black = "^0.3.6"
|
||||||
|
mypy = "^1.1.1"
|
||||||
|
types-oauthlib = "^3.2.0.7"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core>=1.0.0"]
|
requires = ["poetry-core>=1.0.0"]
|
||||||
build-backend = "poetry.core.masonry.api"
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
[tool.ruff.lint]
|
|
||||||
select = ["E", "F", "I"]
|
|
||||||
|
|
||||||
[tool.djlint]
|
[tool.djlint]
|
||||||
extension = "j2"
|
extension = "j2"
|
||||||
profile = "jinja"
|
profile = "jinja"
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"ignorePaths": [".drone.yml"]
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user