Merge pull request 'v4: Personal calendar' (#168) from v4 into master
All checks were successful
divent / lint (push) Successful in 1m48s
divent / docker (push) Successful in 38s
divent / pypi (push) Successful in 1m15s

Reviewed-on: #168
This commit is contained in:
Michel Roux 2023-11-07 00:40:57 +00:00
commit 38bf85d255
11 changed files with 150 additions and 114 deletions

View File

@ -1,4 +1,4 @@
FROM python:3.11.5 as build FROM python:3.11.6 as build
WORKDIR /app WORKDIR /app
COPY . . COPY . .

View File

@ -5,7 +5,8 @@ from functools import wraps
from os import getenv, path from os import getenv, path
from typing import Dict, Optional from typing import Dict, Optional
from disnake import Client, Guild from disnake import Asset, Client, Guild
from disnake.guild_scheduled_event import GuildScheduledEvent
from dotenv import load_dotenv from dotenv import load_dotenv
from ics import Calendar, ContentLine, Event from ics import Calendar, ContentLine, Event
from ics.alarm import DisplayAlarm from ics.alarm import DisplayAlarm
@ -116,17 +117,11 @@ 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(),
) )
@ -179,12 +174,10 @@ async def callback():
@app.route("/guilds") @app.route("/guilds")
@login_required @login_required
async def guilds(): async def guilds():
guild = get_guild_by_id(request.args.get("guild")) guild = request.args.get("guild")
if guild: if guild:
return redirect( return redirect(url_for(".subscribe", entity_id=guild))
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"))
@ -200,17 +193,19 @@ async def guilds():
common_guilds.append(bot_guild) common_guilds.append(bot_guild)
return await render_template( return await render_template(
"guilds.html.j2", user=user, common_guilds=common_guilds "guilds.html.j2",
user=user,
avatar=Asset._from_avatar(None, user["id"], user["avatar"]),
common_guilds=common_guilds,
) )
@app.route("/subscribe/<guild_id>") @app.route("/subscribe/<entity_id>")
@login_required @login_required
async def subscribe(guild_id: str): async def subscribe(entity_id: str):
guild = get_guild_by_id(guild_id) guild = get_guild_by_id(entity_id)
if guild is None:
return redirect(url_for(".login"))
if guild:
try: try:
discord = make_session(token=session.get("oauth2_token")) discord = make_session(token=session.get("oauth2_token"))
user_guilds = discord.get(f"{API_BASE_URL}/users/@me/guilds").json() user_guilds = discord.get(f"{API_BASE_URL}/users/@me/guilds").json()
@ -220,36 +215,32 @@ async def subscribe(guild_id: str):
if not any(str(guild.id) == user_guild["id"] for user_guild in user_guilds): if not any(str(guild.id) == user_guild["id"] for user_guild in user_guilds):
return redirect(url_for(".login")) return redirect(url_for(".login"))
return await render_template("subscribe.html.j2", guild=guild) return await render_template(
"subscribe.html.j2",
avatar=guild.icon,
entity_id=guild.vanity_url_code or guild.id,
)
user = await client.get_or_fetch_user(int(entity_id))
if user and str(user.id) == entity_id:
return await render_template(
"subscribe.html.j2", avatar=user.avatar, entity_id=user.id
)
@app.route("/<guild_id>.ics")
async def ical(guild_id: str):
guild = get_guild_by_id(guild_id)
if guild is None:
return redirect(url_for(".login")) return redirect(url_for(".login"))
calendar = Calendar()
calendar.extra.append(ContentLine(name="REFRESH-INTERVAL", value="PT1H")) def make_event(scheduled_event: GuildScheduledEvent) -> Event:
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 = Event()
event.summary = scheduled_event.name event.summary = scheduled_event.name
event.begin = scheduled_event.scheduled_start_time event.begin = scheduled_event.scheduled_start_time
event.end = scheduled_event.scheduled_end_time event.end = scheduled_event.scheduled_end_time
event.duration = timedelta(hours=2) event.duration = timedelta(hours=2)
event.uid = str(scheduled_event.id) event.uid = str(scheduled_event.id)
event.description = scheduled_event.description event.description = scheduled_event.description
event.url = f"https://discord.com/events/{guild_id}/{scheduled_event.id}" event.url = scheduled_event.url
event.location = ( event.location = (
scheduled_event.entity_metadata.location scheduled_event.entity_metadata.location
if scheduled_event.entity_metadata if scheduled_event.entity_metadata
@ -260,10 +251,63 @@ async def ical(guild_id: str):
alarm.trigger = timedelta(hours=-1) alarm.trigger = timedelta(hours=-1)
event.alarms.append(alarm) 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) calendar.events.append(event)
return calendar.serialize() return calendar.serialize()
user = await client.get_or_fetch_user(int(entity_id))
if user:
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=client.user.display_name))
calendar.extra.append(
ContentLine(name="X-WR-CALNAME", value=client.user.display_name)
)
for guild in client.guilds:
if await guild.get_or_fetch_member(int(entity_id)):
for scheduled_event in guild.scheduled_events:
if user.id in [
member.id
for member in await scheduled_event.fetch_users().flatten()
]:
event = make_event(scheduled_event)
calendar.events.append(event)
return calendar.serialize()
return redirect(url_for(".login"))
def __main__(): def __main__():
quart_task = client.loop.create_task(app.run_task("0.0.0.0")) quart_task = client.loop.create_task(app.run_task("0.0.0.0"))

View File

@ -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">

View File

@ -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 %}

View File

@ -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>

View File

@ -4,32 +4,30 @@
<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="{{ cdn_avatar_url(user.id, user.avatar) }}" <img src="{{ avatar.url }}"
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>
<h3>{{ _('Choose a server:') }}</h3> <h2>{{ _("The discord scheduled event calendar generator") }}</h2>
<select name="guild" class="black_input"> <hr>
<option> <a class="button" href="{{ url_for(".guilds", guild=user.id) }}">{{ _("For all your servers") }}</a>
&nbsp; <div class="hr-sect">{{ _("OR") }}</div>
</option> <select name="guild" class="black_input" onchange="this.form.submit()">
<option>&nbsp;</option>
{% for guild in common_guilds %} {% for guild in common_guilds %}
<option value="{{ guild.vanity_url_code|default(guild.id, True) }}"> <option value="{{ guild.vanity_url_code|default(guild.id, True) }}">{{ guild.name }}</option>
{{ 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>
@ -48,8 +46,5 @@
</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 %}

View File

@ -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>

View File

@ -3,25 +3,22 @@
<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="{{ guild.icon.url }}" <img src="{{ avatar.url }}" alt="{{ _("Avatar") }}" width="80" height="80" />
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 }}/{{ guild.vanity_url_code|default(guild.id, True) }}.ics"> href="https://calendar.google.com/calendar/u/0/r?cid=webcal://{{ request.host }}/{{ entity_id }}.ics">
<i class="fa fa-google"></i> <i class="fa fa-google"></i>
{{ _("Subscribe to") }} Google {{ _("Subscribe to") }} Google
</a> </a>
@ -29,14 +26,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 }}/{{ guild.vanity_url_code|default(guild.id, True) }}.ics"> href="https://outlook.live.com/owa?path=/calendar/action/compose&rru=addsubscription&url=webcal://{{ request.host }}/{{ entity_id }}.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 }}/{{ guild.vanity_url_code|default(guild.id, True) }}.ics"> <a class="button" target="_blank" href="webcal://{{ request.host }}/{{ entity_id }}.ics">
<i class="fa fa-apple"></i> <i class="fa fa-apple"></i>
{{ _("Subscribe to") }} Apple {{ _("Subscribe to") }} Apple
</a> </a>
@ -47,7 +44,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 }}/{{ guild.vanity_url_code|default(guild.id, True) }}.ics"/> <input type="text" readonly class="black_input" value="webcal://{{ request.host }}/{{ entity_id }}.ics"/>
{# djlint:on #} {# djlint:on #}
</div> </div>
</div> </div>

View File

@ -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",
"Choose a server:": "Choisi un serveur :", "For all your servers": "Pour tous tes serveurs",
"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",

4
poetry.lock generated
View File

@ -1649,5 +1649,5 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.8.1" python-versions = ">=3.8.1,<3.12"
content-hash = "e4463ad986a3f8fafe36af53b741fe71dd1a78d65efb3e4240ac422688fca367" content-hash = "30113574429e0ed0539ebe2850264eb819fb2a7af92310402c9eccbca33487ed"

View File

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "divent" name = "divent"
version = "3.4.4" version = "4.0.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 <xefir@crystalyx.net>"]
license = "WTFPL" license = "WTFPL"
@ -12,7 +12,7 @@ repository = "https://git.crystalyx.net/Xefir/Divent"
divent = 'divent.bot:__main__' divent = 'divent.bot:__main__'
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.8.1" python = ">=3.8.1,<3.12"
disnake = "^2.9.1" disnake = "^2.9.1"
ics = "0.8.0.dev0" ics = "0.8.0.dev0"
python-dotenv = "^1.0.0" python-dotenv = "^1.0.0"