[tasks] Add oauth flow for Todoist

This commit is contained in:
2024-10-14 20:07:53 -04:00
parent 7604327ca9
commit 7954765b73
14 changed files with 777 additions and 37 deletions

View File

@ -21,6 +21,8 @@ VROBBLER_THEAUDIODB_API_KEY="<key>"
VROBBLER_IGDB_CLIENT_ID="<id>" VROBBLER_IGDB_CLIENT_ID="<id>"
VROBBLER_IGDB_CLIENT_SECRET="<key>" VROBBLER_IGDB_CLIENT_SECRET="<key>"
VROBBLER_COMICVINE_API_KEY="<key>" VROBBLER_COMICVINE_API_KEY="<key>"
VROBBLER_TODOIST_CLIENT_ID="<id>"
VROBBLER_TODOIST_CLIENT_SECRET="<key>"
# Storages # Storages
# VROBBLER_DATABASE_URL="postgres://USER:PASSWORD@HOST:PORT/NAME" # VROBBLER_DATABASE_URL="postgres://USER:PASSWORD@HOST:PORT/NAME"

View File

@ -12,4 +12,6 @@ class UserProfileAdmin(admin.ModelAdmin):
"twitch_client_secret", "twitch_client_secret",
"lastfm_password", "lastfm_password",
"archivebox_password", "archivebox_password",
"todoist_auth_key",
"todoist_state",
) )

View File

@ -0,0 +1,612 @@
# Generated by Django 4.2.16 on 2024-10-14 23:23
from django.db import migrations, models
import encrypted_field.fields
class Migration(migrations.Migration):
dependencies = [
("profiles", "0016_alter_userprofile_timezone"),
]
operations = [
migrations.AddField(
model_name="userprofile",
name="todoist_auth_key",
field=encrypted_field.fields.EncryptedField(blank=True, null=True),
),
migrations.AddField(
model_name="userprofile",
name="todoist_state",
field=encrypted_field.fields.EncryptedField(blank=True, null=True),
),
migrations.AlterField(
model_name="userprofile",
name="timezone",
field=models.CharField(
choices=[
("Pacific/Midway", "(GMT-1100) Pacific/Midway"),
("Pacific/Niue", "(GMT-1100) Pacific/Niue"),
("Pacific/Pago_Pago", "(GMT-1100) Pacific/Pago_Pago"),
("Pacific/Honolulu", "(GMT-1000) Pacific/Honolulu"),
("Pacific/Rarotonga", "(GMT-1000) Pacific/Rarotonga"),
("Pacific/Tahiti", "(GMT-1000) Pacific/Tahiti"),
("US/Hawaii", "(GMT-1000) US/Hawaii"),
("Pacific/Marquesas", "(GMT-0930) Pacific/Marquesas"),
("America/Adak", "(GMT-0900) America/Adak"),
("Pacific/Gambier", "(GMT-0900) Pacific/Gambier"),
("America/Anchorage", "(GMT-0800) America/Anchorage"),
("America/Juneau", "(GMT-0800) America/Juneau"),
("America/Metlakatla", "(GMT-0800) America/Metlakatla"),
("America/Nome", "(GMT-0800) America/Nome"),
("America/Sitka", "(GMT-0800) America/Sitka"),
("America/Yakutat", "(GMT-0800) America/Yakutat"),
("Pacific/Pitcairn", "(GMT-0800) Pacific/Pitcairn"),
("US/Alaska", "(GMT-0800) US/Alaska"),
("America/Creston", "(GMT-0700) America/Creston"),
("America/Dawson", "(GMT-0700) America/Dawson"),
(
"America/Dawson_Creek",
"(GMT-0700) America/Dawson_Creek",
),
("America/Fort_Nelson", "(GMT-0700) America/Fort_Nelson"),
("America/Hermosillo", "(GMT-0700) America/Hermosillo"),
("America/Los_Angeles", "(GMT-0700) America/Los_Angeles"),
("America/Mazatlan", "(GMT-0700) America/Mazatlan"),
("America/Phoenix", "(GMT-0700) America/Phoenix"),
("America/Tijuana", "(GMT-0700) America/Tijuana"),
("America/Vancouver", "(GMT-0700) America/Vancouver"),
("America/Whitehorse", "(GMT-0700) America/Whitehorse"),
("Canada/Pacific", "(GMT-0700) Canada/Pacific"),
("US/Arizona", "(GMT-0700) US/Arizona"),
("US/Pacific", "(GMT-0700) US/Pacific"),
(
"America/Bahia_Banderas",
"(GMT-0600) America/Bahia_Banderas",
),
("America/Belize", "(GMT-0600) America/Belize"),
("America/Boise", "(GMT-0600) America/Boise"),
(
"America/Cambridge_Bay",
"(GMT-0600) America/Cambridge_Bay",
),
("America/Chihuahua", "(GMT-0600) America/Chihuahua"),
(
"America/Ciudad_Juarez",
"(GMT-0600) America/Ciudad_Juarez",
),
("America/Costa_Rica", "(GMT-0600) America/Costa_Rica"),
("America/Denver", "(GMT-0600) America/Denver"),
("America/Edmonton", "(GMT-0600) America/Edmonton"),
("America/El_Salvador", "(GMT-0600) America/El_Salvador"),
("America/Guatemala", "(GMT-0600) America/Guatemala"),
("America/Inuvik", "(GMT-0600) America/Inuvik"),
("America/Managua", "(GMT-0600) America/Managua"),
("America/Merida", "(GMT-0600) America/Merida"),
("America/Mexico_City", "(GMT-0600) America/Mexico_City"),
("America/Monterrey", "(GMT-0600) America/Monterrey"),
("America/Regina", "(GMT-0600) America/Regina"),
(
"America/Swift_Current",
"(GMT-0600) America/Swift_Current",
),
("America/Tegucigalpa", "(GMT-0600) America/Tegucigalpa"),
("America/Yellowknife", "(GMT-0600) America/Yellowknife"),
("Canada/Mountain", "(GMT-0600) Canada/Mountain"),
("Pacific/Galapagos", "(GMT-0600) Pacific/Galapagos"),
("US/Mountain", "(GMT-0600) US/Mountain"),
("America/Atikokan", "(GMT-0500) America/Atikokan"),
("America/Bogota", "(GMT-0500) America/Bogota"),
("America/Cancun", "(GMT-0500) America/Cancun"),
("America/Cayman", "(GMT-0500) America/Cayman"),
("America/Chicago", "(GMT-0500) America/Chicago"),
("America/Eirunepe", "(GMT-0500) America/Eirunepe"),
("America/Guayaquil", "(GMT-0500) America/Guayaquil"),
(
"America/Indiana/Knox",
"(GMT-0500) America/Indiana/Knox",
),
(
"America/Indiana/Tell_City",
"(GMT-0500) America/Indiana/Tell_City",
),
("America/Jamaica", "(GMT-0500) America/Jamaica"),
("America/Lima", "(GMT-0500) America/Lima"),
("America/Matamoros", "(GMT-0500) America/Matamoros"),
("America/Menominee", "(GMT-0500) America/Menominee"),
(
"America/North_Dakota/Beulah",
"(GMT-0500) America/North_Dakota/Beulah",
),
(
"America/North_Dakota/Center",
"(GMT-0500) America/North_Dakota/Center",
),
(
"America/North_Dakota/New_Salem",
"(GMT-0500) America/North_Dakota/New_Salem",
),
("America/Ojinaga", "(GMT-0500) America/Ojinaga"),
("America/Panama", "(GMT-0500) America/Panama"),
(
"America/Rankin_Inlet",
"(GMT-0500) America/Rankin_Inlet",
),
("America/Resolute", "(GMT-0500) America/Resolute"),
("America/Rio_Branco", "(GMT-0500) America/Rio_Branco"),
("America/Winnipeg", "(GMT-0500) America/Winnipeg"),
("Canada/Central", "(GMT-0500) Canada/Central"),
("Pacific/Easter", "(GMT-0500) Pacific/Easter"),
("US/Central", "(GMT-0500) US/Central"),
("America/Anguilla", "(GMT-0400) America/Anguilla"),
("America/Antigua", "(GMT-0400) America/Antigua"),
("America/Aruba", "(GMT-0400) America/Aruba"),
("America/Barbados", "(GMT-0400) America/Barbados"),
(
"America/Blanc-Sablon",
"(GMT-0400) America/Blanc-Sablon",
),
("America/Boa_Vista", "(GMT-0400) America/Boa_Vista"),
(
"America/Campo_Grande",
"(GMT-0400) America/Campo_Grande",
),
("America/Caracas", "(GMT-0400) America/Caracas"),
("America/Cuiaba", "(GMT-0400) America/Cuiaba"),
("America/Curacao", "(GMT-0400) America/Curacao"),
("America/Detroit", "(GMT-0400) America/Detroit"),
("America/Dominica", "(GMT-0400) America/Dominica"),
("America/Grand_Turk", "(GMT-0400) America/Grand_Turk"),
("America/Grenada", "(GMT-0400) America/Grenada"),
("America/Guadeloupe", "(GMT-0400) America/Guadeloupe"),
("America/Guyana", "(GMT-0400) America/Guyana"),
("America/Havana", "(GMT-0400) America/Havana"),
(
"America/Indiana/Indianapolis",
"(GMT-0400) America/Indiana/Indianapolis",
),
(
"America/Indiana/Marengo",
"(GMT-0400) America/Indiana/Marengo",
),
(
"America/Indiana/Petersburg",
"(GMT-0400) America/Indiana/Petersburg",
),
(
"America/Indiana/Vevay",
"(GMT-0400) America/Indiana/Vevay",
),
(
"America/Indiana/Vincennes",
"(GMT-0400) America/Indiana/Vincennes",
),
(
"America/Indiana/Winamac",
"(GMT-0400) America/Indiana/Winamac",
),
("America/Iqaluit", "(GMT-0400) America/Iqaluit"),
(
"America/Kentucky/Louisville",
"(GMT-0400) America/Kentucky/Louisville",
),
(
"America/Kentucky/Monticello",
"(GMT-0400) America/Kentucky/Monticello",
),
("America/Kralendijk", "(GMT-0400) America/Kralendijk"),
("America/La_Paz", "(GMT-0400) America/La_Paz"),
(
"America/Lower_Princes",
"(GMT-0400) America/Lower_Princes",
),
("America/Manaus", "(GMT-0400) America/Manaus"),
("America/Marigot", "(GMT-0400) America/Marigot"),
("America/Martinique", "(GMT-0400) America/Martinique"),
("America/Montserrat", "(GMT-0400) America/Montserrat"),
("America/Nassau", "(GMT-0400) America/Nassau"),
("America/New_York", "(GMT-0400) America/New_York"),
(
"America/Port-au-Prince",
"(GMT-0400) America/Port-au-Prince",
),
(
"America/Port_of_Spain",
"(GMT-0400) America/Port_of_Spain",
),
("America/Porto_Velho", "(GMT-0400) America/Porto_Velho"),
("America/Puerto_Rico", "(GMT-0400) America/Puerto_Rico"),
(
"America/Santo_Domingo",
"(GMT-0400) America/Santo_Domingo",
),
(
"America/St_Barthelemy",
"(GMT-0400) America/St_Barthelemy",
),
("America/St_Kitts", "(GMT-0400) America/St_Kitts"),
("America/St_Lucia", "(GMT-0400) America/St_Lucia"),
("America/St_Thomas", "(GMT-0400) America/St_Thomas"),
("America/St_Vincent", "(GMT-0400) America/St_Vincent"),
("America/Toronto", "(GMT-0400) America/Toronto"),
("America/Tortola", "(GMT-0400) America/Tortola"),
("Canada/Eastern", "(GMT-0400) Canada/Eastern"),
("US/Eastern", "(GMT-0400) US/Eastern"),
("America/Araguaina", "(GMT-0300) America/Araguaina"),
(
"America/Argentina/Buenos_Aires",
"(GMT-0300) America/Argentina/Buenos_Aires",
),
(
"America/Argentina/Catamarca",
"(GMT-0300) America/Argentina/Catamarca",
),
(
"America/Argentina/Cordoba",
"(GMT-0300) America/Argentina/Cordoba",
),
(
"America/Argentina/Jujuy",
"(GMT-0300) America/Argentina/Jujuy",
),
(
"America/Argentina/La_Rioja",
"(GMT-0300) America/Argentina/La_Rioja",
),
(
"America/Argentina/Mendoza",
"(GMT-0300) America/Argentina/Mendoza",
),
(
"America/Argentina/Rio_Gallegos",
"(GMT-0300) America/Argentina/Rio_Gallegos",
),
(
"America/Argentina/Salta",
"(GMT-0300) America/Argentina/Salta",
),
(
"America/Argentina/San_Juan",
"(GMT-0300) America/Argentina/San_Juan",
),
(
"America/Argentina/San_Luis",
"(GMT-0300) America/Argentina/San_Luis",
),
(
"America/Argentina/Tucuman",
"(GMT-0300) America/Argentina/Tucuman",
),
(
"America/Argentina/Ushuaia",
"(GMT-0300) America/Argentina/Ushuaia",
),
("America/Asuncion", "(GMT-0300) America/Asuncion"),
("America/Bahia", "(GMT-0300) America/Bahia"),
("America/Belem", "(GMT-0300) America/Belem"),
("America/Cayenne", "(GMT-0300) America/Cayenne"),
("America/Fortaleza", "(GMT-0300) America/Fortaleza"),
("America/Glace_Bay", "(GMT-0300) America/Glace_Bay"),
("America/Goose_Bay", "(GMT-0300) America/Goose_Bay"),
("America/Halifax", "(GMT-0300) America/Halifax"),
("America/Maceio", "(GMT-0300) America/Maceio"),
("America/Moncton", "(GMT-0300) America/Moncton"),
("America/Montevideo", "(GMT-0300) America/Montevideo"),
("America/Paramaribo", "(GMT-0300) America/Paramaribo"),
(
"America/Punta_Arenas",
"(GMT-0300) America/Punta_Arenas",
),
("America/Recife", "(GMT-0300) America/Recife"),
("America/Santarem", "(GMT-0300) America/Santarem"),
("America/Santiago", "(GMT-0300) America/Santiago"),
("America/Sao_Paulo", "(GMT-0300) America/Sao_Paulo"),
("America/Thule", "(GMT-0300) America/Thule"),
("Antarctica/Palmer", "(GMT-0300) Antarctica/Palmer"),
("Antarctica/Rothera", "(GMT-0300) Antarctica/Rothera"),
("Atlantic/Bermuda", "(GMT-0300) Atlantic/Bermuda"),
("Atlantic/Stanley", "(GMT-0300) Atlantic/Stanley"),
("Canada/Atlantic", "(GMT-0300) Canada/Atlantic"),
("America/St_Johns", "(GMT-0230) America/St_Johns"),
("Canada/Newfoundland", "(GMT-0230) Canada/Newfoundland"),
("America/Miquelon", "(GMT-0200) America/Miquelon"),
("America/Noronha", "(GMT-0200) America/Noronha"),
("America/Nuuk", "(GMT-0200) America/Nuuk"),
(
"Atlantic/South_Georgia",
"(GMT-0200) Atlantic/South_Georgia",
),
("Atlantic/Cape_Verde", "(GMT-0100) Atlantic/Cape_Verde"),
("Africa/Abidjan", "(GMT+0000) Africa/Abidjan"),
("Africa/Accra", "(GMT+0000) Africa/Accra"),
("Africa/Bamako", "(GMT+0000) Africa/Bamako"),
("Africa/Banjul", "(GMT+0000) Africa/Banjul"),
("Africa/Bissau", "(GMT+0000) Africa/Bissau"),
("Africa/Conakry", "(GMT+0000) Africa/Conakry"),
("Africa/Dakar", "(GMT+0000) Africa/Dakar"),
("Africa/Freetown", "(GMT+0000) Africa/Freetown"),
("Africa/Lome", "(GMT+0000) Africa/Lome"),
("Africa/Monrovia", "(GMT+0000) Africa/Monrovia"),
("Africa/Nouakchott", "(GMT+0000) Africa/Nouakchott"),
("Africa/Ouagadougou", "(GMT+0000) Africa/Ouagadougou"),
("Africa/Sao_Tome", "(GMT+0000) Africa/Sao_Tome"),
(
"America/Danmarkshavn",
"(GMT+0000) America/Danmarkshavn",
),
(
"America/Scoresbysund",
"(GMT+0000) America/Scoresbysund",
),
("Atlantic/Azores", "(GMT+0000) Atlantic/Azores"),
("Atlantic/Reykjavik", "(GMT+0000) Atlantic/Reykjavik"),
("Atlantic/St_Helena", "(GMT+0000) Atlantic/St_Helena"),
("GMT", "(GMT+0000) GMT"),
("UTC", "(GMT+0000) UTC"),
("Africa/Algiers", "(GMT+0100) Africa/Algiers"),
("Africa/Bangui", "(GMT+0100) Africa/Bangui"),
("Africa/Brazzaville", "(GMT+0100) Africa/Brazzaville"),
("Africa/Casablanca", "(GMT+0100) Africa/Casablanca"),
("Africa/Douala", "(GMT+0100) Africa/Douala"),
("Africa/El_Aaiun", "(GMT+0100) Africa/El_Aaiun"),
("Africa/Kinshasa", "(GMT+0100) Africa/Kinshasa"),
("Africa/Lagos", "(GMT+0100) Africa/Lagos"),
("Africa/Libreville", "(GMT+0100) Africa/Libreville"),
("Africa/Luanda", "(GMT+0100) Africa/Luanda"),
("Africa/Malabo", "(GMT+0100) Africa/Malabo"),
("Africa/Ndjamena", "(GMT+0100) Africa/Ndjamena"),
("Africa/Niamey", "(GMT+0100) Africa/Niamey"),
("Africa/Porto-Novo", "(GMT+0100) Africa/Porto-Novo"),
("Africa/Tunis", "(GMT+0100) Africa/Tunis"),
("Atlantic/Canary", "(GMT+0100) Atlantic/Canary"),
("Atlantic/Faroe", "(GMT+0100) Atlantic/Faroe"),
("Atlantic/Madeira", "(GMT+0100) Atlantic/Madeira"),
("Europe/Dublin", "(GMT+0100) Europe/Dublin"),
("Europe/Guernsey", "(GMT+0100) Europe/Guernsey"),
("Europe/Isle_of_Man", "(GMT+0100) Europe/Isle_of_Man"),
("Europe/Jersey", "(GMT+0100) Europe/Jersey"),
("Europe/Lisbon", "(GMT+0100) Europe/Lisbon"),
("Europe/London", "(GMT+0100) Europe/London"),
("Africa/Blantyre", "(GMT+0200) Africa/Blantyre"),
("Africa/Bujumbura", "(GMT+0200) Africa/Bujumbura"),
("Africa/Cairo", "(GMT+0200) Africa/Cairo"),
("Africa/Ceuta", "(GMT+0200) Africa/Ceuta"),
("Africa/Gaborone", "(GMT+0200) Africa/Gaborone"),
("Africa/Harare", "(GMT+0200) Africa/Harare"),
("Africa/Johannesburg", "(GMT+0200) Africa/Johannesburg"),
("Africa/Juba", "(GMT+0200) Africa/Juba"),
("Africa/Khartoum", "(GMT+0200) Africa/Khartoum"),
("Africa/Kigali", "(GMT+0200) Africa/Kigali"),
("Africa/Lubumbashi", "(GMT+0200) Africa/Lubumbashi"),
("Africa/Lusaka", "(GMT+0200) Africa/Lusaka"),
("Africa/Maputo", "(GMT+0200) Africa/Maputo"),
("Africa/Maseru", "(GMT+0200) Africa/Maseru"),
("Africa/Mbabane", "(GMT+0200) Africa/Mbabane"),
("Africa/Tripoli", "(GMT+0200) Africa/Tripoli"),
("Africa/Windhoek", "(GMT+0200) Africa/Windhoek"),
("Antarctica/Troll", "(GMT+0200) Antarctica/Troll"),
("Arctic/Longyearbyen", "(GMT+0200) Arctic/Longyearbyen"),
("Europe/Amsterdam", "(GMT+0200) Europe/Amsterdam"),
("Europe/Andorra", "(GMT+0200) Europe/Andorra"),
("Europe/Belgrade", "(GMT+0200) Europe/Belgrade"),
("Europe/Berlin", "(GMT+0200) Europe/Berlin"),
("Europe/Bratislava", "(GMT+0200) Europe/Bratislava"),
("Europe/Brussels", "(GMT+0200) Europe/Brussels"),
("Europe/Budapest", "(GMT+0200) Europe/Budapest"),
("Europe/Busingen", "(GMT+0200) Europe/Busingen"),
("Europe/Copenhagen", "(GMT+0200) Europe/Copenhagen"),
("Europe/Gibraltar", "(GMT+0200) Europe/Gibraltar"),
("Europe/Kaliningrad", "(GMT+0200) Europe/Kaliningrad"),
("Europe/Ljubljana", "(GMT+0200) Europe/Ljubljana"),
("Europe/Luxembourg", "(GMT+0200) Europe/Luxembourg"),
("Europe/Madrid", "(GMT+0200) Europe/Madrid"),
("Europe/Malta", "(GMT+0200) Europe/Malta"),
("Europe/Monaco", "(GMT+0200) Europe/Monaco"),
("Europe/Oslo", "(GMT+0200) Europe/Oslo"),
("Europe/Paris", "(GMT+0200) Europe/Paris"),
("Europe/Podgorica", "(GMT+0200) Europe/Podgorica"),
("Europe/Prague", "(GMT+0200) Europe/Prague"),
("Europe/Rome", "(GMT+0200) Europe/Rome"),
("Europe/San_Marino", "(GMT+0200) Europe/San_Marino"),
("Europe/Sarajevo", "(GMT+0200) Europe/Sarajevo"),
("Europe/Skopje", "(GMT+0200) Europe/Skopje"),
("Europe/Stockholm", "(GMT+0200) Europe/Stockholm"),
("Europe/Tirane", "(GMT+0200) Europe/Tirane"),
("Europe/Vaduz", "(GMT+0200) Europe/Vaduz"),
("Europe/Vatican", "(GMT+0200) Europe/Vatican"),
("Europe/Vienna", "(GMT+0200) Europe/Vienna"),
("Europe/Warsaw", "(GMT+0200) Europe/Warsaw"),
("Europe/Zagreb", "(GMT+0200) Europe/Zagreb"),
("Europe/Zurich", "(GMT+0200) Europe/Zurich"),
("Africa/Addis_Ababa", "(GMT+0300) Africa/Addis_Ababa"),
("Africa/Asmara", "(GMT+0300) Africa/Asmara"),
(
"Africa/Dar_es_Salaam",
"(GMT+0300) Africa/Dar_es_Salaam",
),
("Africa/Djibouti", "(GMT+0300) Africa/Djibouti"),
("Africa/Kampala", "(GMT+0300) Africa/Kampala"),
("Africa/Mogadishu", "(GMT+0300) Africa/Mogadishu"),
("Africa/Nairobi", "(GMT+0300) Africa/Nairobi"),
("Antarctica/Syowa", "(GMT+0300) Antarctica/Syowa"),
("Asia/Aden", "(GMT+0300) Asia/Aden"),
("Asia/Amman", "(GMT+0300) Asia/Amman"),
("Asia/Baghdad", "(GMT+0300) Asia/Baghdad"),
("Asia/Bahrain", "(GMT+0300) Asia/Bahrain"),
("Asia/Beirut", "(GMT+0300) Asia/Beirut"),
("Asia/Damascus", "(GMT+0300) Asia/Damascus"),
("Asia/Famagusta", "(GMT+0300) Asia/Famagusta"),
("Asia/Gaza", "(GMT+0300) Asia/Gaza"),
("Asia/Hebron", "(GMT+0300) Asia/Hebron"),
("Asia/Jerusalem", "(GMT+0300) Asia/Jerusalem"),
("Asia/Kuwait", "(GMT+0300) Asia/Kuwait"),
("Asia/Nicosia", "(GMT+0300) Asia/Nicosia"),
("Asia/Qatar", "(GMT+0300) Asia/Qatar"),
("Asia/Riyadh", "(GMT+0300) Asia/Riyadh"),
("Europe/Athens", "(GMT+0300) Europe/Athens"),
("Europe/Bucharest", "(GMT+0300) Europe/Bucharest"),
("Europe/Chisinau", "(GMT+0300) Europe/Chisinau"),
("Europe/Helsinki", "(GMT+0300) Europe/Helsinki"),
("Europe/Istanbul", "(GMT+0300) Europe/Istanbul"),
("Europe/Kirov", "(GMT+0300) Europe/Kirov"),
("Europe/Kyiv", "(GMT+0300) Europe/Kyiv"),
("Europe/Mariehamn", "(GMT+0300) Europe/Mariehamn"),
("Europe/Minsk", "(GMT+0300) Europe/Minsk"),
("Europe/Moscow", "(GMT+0300) Europe/Moscow"),
("Europe/Riga", "(GMT+0300) Europe/Riga"),
("Europe/Simferopol", "(GMT+0300) Europe/Simferopol"),
("Europe/Sofia", "(GMT+0300) Europe/Sofia"),
("Europe/Tallinn", "(GMT+0300) Europe/Tallinn"),
("Europe/Vilnius", "(GMT+0300) Europe/Vilnius"),
("Europe/Volgograd", "(GMT+0300) Europe/Volgograd"),
("Indian/Antananarivo", "(GMT+0300) Indian/Antananarivo"),
("Indian/Comoro", "(GMT+0300) Indian/Comoro"),
("Indian/Mayotte", "(GMT+0300) Indian/Mayotte"),
("Asia/Tehran", "(GMT+0330) Asia/Tehran"),
("Asia/Baku", "(GMT+0400) Asia/Baku"),
("Asia/Dubai", "(GMT+0400) Asia/Dubai"),
("Asia/Muscat", "(GMT+0400) Asia/Muscat"),
("Asia/Tbilisi", "(GMT+0400) Asia/Tbilisi"),
("Asia/Yerevan", "(GMT+0400) Asia/Yerevan"),
("Europe/Astrakhan", "(GMT+0400) Europe/Astrakhan"),
("Europe/Samara", "(GMT+0400) Europe/Samara"),
("Europe/Saratov", "(GMT+0400) Europe/Saratov"),
("Europe/Ulyanovsk", "(GMT+0400) Europe/Ulyanovsk"),
("Indian/Mahe", "(GMT+0400) Indian/Mahe"),
("Indian/Mauritius", "(GMT+0400) Indian/Mauritius"),
("Indian/Reunion", "(GMT+0400) Indian/Reunion"),
("Asia/Kabul", "(GMT+0430) Asia/Kabul"),
("Antarctica/Mawson", "(GMT+0500) Antarctica/Mawson"),
("Asia/Aqtau", "(GMT+0500) Asia/Aqtau"),
("Asia/Aqtobe", "(GMT+0500) Asia/Aqtobe"),
("Asia/Ashgabat", "(GMT+0500) Asia/Ashgabat"),
("Asia/Atyrau", "(GMT+0500) Asia/Atyrau"),
("Asia/Dushanbe", "(GMT+0500) Asia/Dushanbe"),
("Asia/Karachi", "(GMT+0500) Asia/Karachi"),
("Asia/Oral", "(GMT+0500) Asia/Oral"),
("Asia/Qyzylorda", "(GMT+0500) Asia/Qyzylorda"),
("Asia/Samarkand", "(GMT+0500) Asia/Samarkand"),
("Asia/Tashkent", "(GMT+0500) Asia/Tashkent"),
("Asia/Yekaterinburg", "(GMT+0500) Asia/Yekaterinburg"),
("Indian/Kerguelen", "(GMT+0500) Indian/Kerguelen"),
("Indian/Maldives", "(GMT+0500) Indian/Maldives"),
("Asia/Colombo", "(GMT+0530) Asia/Colombo"),
("Asia/Kolkata", "(GMT+0530) Asia/Kolkata"),
("Asia/Kathmandu", "(GMT+0545) Asia/Kathmandu"),
("Antarctica/Vostok", "(GMT+0600) Antarctica/Vostok"),
("Asia/Almaty", "(GMT+0600) Asia/Almaty"),
("Asia/Bishkek", "(GMT+0600) Asia/Bishkek"),
("Asia/Dhaka", "(GMT+0600) Asia/Dhaka"),
("Asia/Omsk", "(GMT+0600) Asia/Omsk"),
("Asia/Qostanay", "(GMT+0600) Asia/Qostanay"),
("Asia/Thimphu", "(GMT+0600) Asia/Thimphu"),
("Asia/Urumqi", "(GMT+0600) Asia/Urumqi"),
("Indian/Chagos", "(GMT+0600) Indian/Chagos"),
("Asia/Yangon", "(GMT+0630) Asia/Yangon"),
("Indian/Cocos", "(GMT+0630) Indian/Cocos"),
("Antarctica/Davis", "(GMT+0700) Antarctica/Davis"),
("Asia/Bangkok", "(GMT+0700) Asia/Bangkok"),
("Asia/Barnaul", "(GMT+0700) Asia/Barnaul"),
("Asia/Ho_Chi_Minh", "(GMT+0700) Asia/Ho_Chi_Minh"),
("Asia/Hovd", "(GMT+0700) Asia/Hovd"),
("Asia/Jakarta", "(GMT+0700) Asia/Jakarta"),
("Asia/Krasnoyarsk", "(GMT+0700) Asia/Krasnoyarsk"),
("Asia/Novokuznetsk", "(GMT+0700) Asia/Novokuznetsk"),
("Asia/Novosibirsk", "(GMT+0700) Asia/Novosibirsk"),
("Asia/Phnom_Penh", "(GMT+0700) Asia/Phnom_Penh"),
("Asia/Pontianak", "(GMT+0700) Asia/Pontianak"),
("Asia/Tomsk", "(GMT+0700) Asia/Tomsk"),
("Asia/Vientiane", "(GMT+0700) Asia/Vientiane"),
("Indian/Christmas", "(GMT+0700) Indian/Christmas"),
("Asia/Brunei", "(GMT+0800) Asia/Brunei"),
("Asia/Choibalsan", "(GMT+0800) Asia/Choibalsan"),
("Asia/Hong_Kong", "(GMT+0800) Asia/Hong_Kong"),
("Asia/Irkutsk", "(GMT+0800) Asia/Irkutsk"),
("Asia/Kuala_Lumpur", "(GMT+0800) Asia/Kuala_Lumpur"),
("Asia/Kuching", "(GMT+0800) Asia/Kuching"),
("Asia/Macau", "(GMT+0800) Asia/Macau"),
("Asia/Makassar", "(GMT+0800) Asia/Makassar"),
("Asia/Manila", "(GMT+0800) Asia/Manila"),
("Asia/Shanghai", "(GMT+0800) Asia/Shanghai"),
("Asia/Singapore", "(GMT+0800) Asia/Singapore"),
("Asia/Taipei", "(GMT+0800) Asia/Taipei"),
("Asia/Ulaanbaatar", "(GMT+0800) Asia/Ulaanbaatar"),
("Australia/Perth", "(GMT+0800) Australia/Perth"),
("Australia/Eucla", "(GMT+0845) Australia/Eucla"),
("Asia/Chita", "(GMT+0900) Asia/Chita"),
("Asia/Dili", "(GMT+0900) Asia/Dili"),
("Asia/Jayapura", "(GMT+0900) Asia/Jayapura"),
("Asia/Khandyga", "(GMT+0900) Asia/Khandyga"),
("Asia/Pyongyang", "(GMT+0900) Asia/Pyongyang"),
("Asia/Seoul", "(GMT+0900) Asia/Seoul"),
("Asia/Tokyo", "(GMT+0900) Asia/Tokyo"),
("Asia/Yakutsk", "(GMT+0900) Asia/Yakutsk"),
("Pacific/Palau", "(GMT+0900) Pacific/Palau"),
("Australia/Darwin", "(GMT+0930) Australia/Darwin"),
(
"Antarctica/DumontDUrville",
"(GMT+1000) Antarctica/DumontDUrville",
),
("Asia/Ust-Nera", "(GMT+1000) Asia/Ust-Nera"),
("Asia/Vladivostok", "(GMT+1000) Asia/Vladivostok"),
("Australia/Brisbane", "(GMT+1000) Australia/Brisbane"),
("Australia/Lindeman", "(GMT+1000) Australia/Lindeman"),
("Pacific/Chuuk", "(GMT+1000) Pacific/Chuuk"),
("Pacific/Guam", "(GMT+1000) Pacific/Guam"),
(
"Pacific/Port_Moresby",
"(GMT+1000) Pacific/Port_Moresby",
),
("Pacific/Saipan", "(GMT+1000) Pacific/Saipan"),
("Australia/Adelaide", "(GMT+1030) Australia/Adelaide"),
(
"Australia/Broken_Hill",
"(GMT+1030) Australia/Broken_Hill",
),
("Antarctica/Casey", "(GMT+1100) Antarctica/Casey"),
(
"Antarctica/Macquarie",
"(GMT+1100) Antarctica/Macquarie",
),
("Asia/Magadan", "(GMT+1100) Asia/Magadan"),
("Asia/Sakhalin", "(GMT+1100) Asia/Sakhalin"),
("Asia/Srednekolymsk", "(GMT+1100) Asia/Srednekolymsk"),
("Australia/Hobart", "(GMT+1100) Australia/Hobart"),
("Australia/Lord_Howe", "(GMT+1100) Australia/Lord_Howe"),
("Australia/Melbourne", "(GMT+1100) Australia/Melbourne"),
("Australia/Sydney", "(GMT+1100) Australia/Sydney"),
(
"Pacific/Bougainville",
"(GMT+1100) Pacific/Bougainville",
),
("Pacific/Efate", "(GMT+1100) Pacific/Efate"),
("Pacific/Guadalcanal", "(GMT+1100) Pacific/Guadalcanal"),
("Pacific/Kosrae", "(GMT+1100) Pacific/Kosrae"),
("Pacific/Noumea", "(GMT+1100) Pacific/Noumea"),
("Pacific/Pohnpei", "(GMT+1100) Pacific/Pohnpei"),
("Asia/Anadyr", "(GMT+1200) Asia/Anadyr"),
("Asia/Kamchatka", "(GMT+1200) Asia/Kamchatka"),
("Pacific/Fiji", "(GMT+1200) Pacific/Fiji"),
("Pacific/Funafuti", "(GMT+1200) Pacific/Funafuti"),
("Pacific/Kwajalein", "(GMT+1200) Pacific/Kwajalein"),
("Pacific/Majuro", "(GMT+1200) Pacific/Majuro"),
("Pacific/Nauru", "(GMT+1200) Pacific/Nauru"),
("Pacific/Norfolk", "(GMT+1200) Pacific/Norfolk"),
("Pacific/Tarawa", "(GMT+1200) Pacific/Tarawa"),
("Pacific/Wake", "(GMT+1200) Pacific/Wake"),
("Pacific/Wallis", "(GMT+1200) Pacific/Wallis"),
("Antarctica/McMurdo", "(GMT+1300) Antarctica/McMurdo"),
("Pacific/Apia", "(GMT+1300) Pacific/Apia"),
("Pacific/Auckland", "(GMT+1300) Pacific/Auckland"),
("Pacific/Fakaofo", "(GMT+1300) Pacific/Fakaofo"),
("Pacific/Kanton", "(GMT+1300) Pacific/Kanton"),
("Pacific/Tongatapu", "(GMT+1300) Pacific/Tongatapu"),
("Pacific/Chatham", "(GMT+1345) Pacific/Chatham"),
("Pacific/Kiritimati", "(GMT+1400) Pacific/Kiritimati"),
],
default="UTC",
max_length=255,
),
),
]

View File

@ -1,7 +1,6 @@
from datetime import timedelta from datetime import timedelta
import pytz import pytz
from django.utils import timezone
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.db import models from django.db import models
from django_extensions.db.models import TimeStampedModel from django_extensions.db.models import TimeStampedModel
@ -32,6 +31,9 @@ class UserProfile(TimeStampedModel):
bgg_username = models.CharField(max_length=255, **BNULL) bgg_username = models.CharField(max_length=255, **BNULL)
todoist_auth_key = EncryptedField(**BNULL)
todoist_state = EncryptedField(**BNULL)
redirect_to_webpage = models.BooleanField(default=True) redirect_to_webpage = models.BooleanField(default=True)
def __str__(self): def __str__(self):

View File

@ -23,20 +23,14 @@ MEDIA_END_PADDING_SECONDS = {
"Video": 3600, # 60 min "Video": 3600, # 60 min
} }
TASK_SOURCE_URL_PATTERNS = [ TODOIST_TASK_URL = "https://app.todoist.com/app/task/{id}"
("https://app.shortcut.com/sure/story/{id}", "Shortcut"),
("https://app.todoist.com/app/task/{id}", "Todoist"),
]
for task_pattern in TASK_SOURCE_URL_PATTERNS:
task_urls = ", ".join([t[0] for t in TASK_SOURCE_URL_PATTERNS])
SCROBBLE_CONTENT_URLS = { SCROBBLE_CONTENT_URLS = {
"-i": "https://www.imdb.com/title/", "-i": "https://www.imdb.com/title/",
"-s": "https://www.thesportsdb.com/event/", "-s": "https://www.thesportsdb.com/event/",
"-g": "https://boardgamegeek.com/boardgame/", "-g": "https://boardgamegeek.com/boardgame/",
"-b": "https://www.amazon.com/", "-b": "https://www.amazon.com/",
"-t": task_urls, "-t": "https://app.todoist.com/app/task/{id}",
} }
EXCLUDE_FROM_NOW_PLAYING = ("GeoLocation",) EXCLUDE_FROM_NOW_PLAYING = ("GeoLocation",)

View File

@ -944,10 +944,8 @@ class Scrobble(TimeStampedModel):
url = "" url = ""
if self.media_type == "Website": if self.media_type == "Website":
url = self.media_obj.url url = self.media_obj.url
if self.media_type == "Task" and self.logdata.source_id: if self.media_type == "Task":
url = self.media_obj.source_url_pattern.format( url = self.media_obj.source_url_for_user(self.user)
id=self.logdata.source_id
)
return url return url
@classmethod @classmethod

View File

@ -25,7 +25,6 @@ from videos.models import Video
from scrobbles.constants import ( from scrobbles.constants import (
MANUAL_SCROBBLE_FNS, MANUAL_SCROBBLE_FNS,
SCROBBLE_CONTENT_URLS, SCROBBLE_CONTENT_URLS,
TASK_SOURCE_URL_PATTERNS,
) )
from tasks.models import Task from tasks.models import Task
from webpages.models import WebPage from webpages.models import WebPage
@ -310,10 +309,6 @@ def manual_scrobble_from_url(url: str, user_id: int) -> Scrobble:
def manual_scrobble_task(url: str, user_id: int): def manual_scrobble_task(url: str, user_id: int):
source_id = re.findall("\d+", url)[0] source_id = re.findall("\d+", url)[0]
if "shortcut" in url:
source = "Shortcut"
title = "Generic Shortcut task"
description = " ".join(url.split("/")[-1].split("-")).capitalize()
if "todoist" in url: if "todoist" in url:
source = "Todoist" source = "Todoist"
title = "Generic Todoist task" title = "Generic Todoist task"

View File

@ -1,5 +1,6 @@
from django.urls import path from django.urls import path
from scrobbles import views from scrobbles import views
from tasks.webhooks import todoist_webhook
app_name = "scrobbles" app_name = "scrobbles"
@ -45,6 +46,7 @@ urlpatterns = [
views.mopidy_webhook, views.mopidy_webhook,
name="mopidy-webhook", name="mopidy-webhook",
), ),
path("webhook/todoist/", todoist_webhook, name="todoist-webhook"),
path("export/", views.export, name="export"), path("export/", views.export, name="export"),
path( path(
"imports/", "imports/",

View File

@ -7,34 +7,27 @@ from django.db import models
from django.urls import reverse from django.urls import reverse
from scrobbles.dataclasses import LongPlayLogData from scrobbles.dataclasses import LongPlayLogData
from scrobbles.mixins import LongPlayScrobblableMixin from scrobbles.mixins import LongPlayScrobblableMixin
from scrobbles.constants import TASK_SOURCE_URL_PATTERNS
BNULL = {"blank": True, "null": True} BNULL = {"blank": True, "null": True}
TODOIST_TASK_URL = "https://app.todoist.com/app/task/{id}"
@dataclass @dataclass
class TaskLogData(LongPlayLogData): class TaskLogData(LongPlayLogData):
description: Optional[str] = None description: Optional[str] = None
project: Optional[str] = None
source_id: Optional[str] = None source_id: Optional[str] = None
serial_scrobble_id: Optional[int] = None serial_scrobble_id: Optional[int] = None
long_play_complete: Optional[bool] = None long_play_complete: Optional[bool] = None
class TaskType(Enum):
PRO = "Professional"
AMATEUR = "Amateur"
class Task(LongPlayScrobblableMixin): class Task(LongPlayScrobblableMixin):
"""Basically a holder for task sources ... Shortcut, JIRA, Todoist, Org-mode """Basically a holder for Todoist Tasks
and any other otherwise generic tasks. and any other otherwise generic tasks.
""" """
source = models.CharField(max_length=255, **BNULL)
source_url_pattern = models.CharField(
max_length=255, choices=TASK_SOURCE_URL_PATTERNS, **BNULL
)
description = models.TextField(**BNULL) description = models.TextField(**BNULL)
def __str__(self): def __str__(self):
@ -47,14 +40,11 @@ class Task(LongPlayScrobblableMixin):
def logdata_cls(self): def logdata_cls(self):
return TaskLogData return TaskLogData
def source_url_for_user(self, user_id): def source_url_for_user(self, user_id) -> str:
url = str(self.source_url_pattern).replace("{id}", "") url = ""
scrobble = self.scrobbles(user_id).first() scrobble = self.scrobbles(user_id).first()
if scrobble.logdata.source_id and self.source_url_pattern: if scrobble:
url = str(self.source_url_pattern).format( url = TODOIST_TASK_URL.format(id=scrobble.logdata.source_id)
id=scrobble.logdata.source_id
)
return url return url
def subtitle_for_user(self, user_id): def subtitle_for_user(self, user_id):

View File

@ -0,0 +1,61 @@
import logging
import secrets
import requests
from django.conf import settings
from django.contrib.auth import get_user_model
from profiles.models import UserProfile
from vrobbler.settings import TODOIST_CLIENT_ID
logger = logging.getLogger(__name__)
User = get_user_model()
TODOIST_OAUTH_START_URL = "https://todoist.com/oauth/authorize?client_id={id}&scope=data:read_write&state=".format(
id=TODOIST_CLIENT_ID
)
TODOIST_OAUTH_TOKEN_URL = "https://todoist.com/oauth/access_token"
def generate_todoist_oauth_url(user_id: int) -> str:
user_profile = User.objects.filter(id=user_id).first().profile
user_profile.todoist_state = secrets.token_hex(16)
user_profile.save(update_fields=["todoist_state"])
return TODOIST_OAUTH_START_URL + user_profile.todoist_state
def get_todoist_access_token(user_id: int, state: str, code: str):
logger.info(
"[get_todoist_access_token] called",
extra={"state": state, "code": code},
)
user_profile = UserProfile.objects.filter(user_id=user_id).first()
if not user_profile:
raise Exception
if user_profile.todoist_state != state:
logger.info(
"[get_todoist_access_token] state mismatch",
extra={"user_id": user_id, "state": state},
)
raise Exception
post_data = {
"client_id": settings.TODOIST_CLIENT_ID,
"client_secret": settings.TODOIST_CLIENT_SECRET,
"code": code,
}
response = requests.post(TODOIST_OAUTH_TOKEN_URL, data=post_data)
if response.status_code == 200:
user_profile.todoist_auth_key = response.json().get("access_token")
user_profile.todoist_state = None
user_profile.save()
logger.info(
"[get_todoist_access_token] finished",
extra={"user_id": user_profile.user.id},
)

View File

@ -11,4 +11,5 @@ urlpatterns = [
views.TaskDetailView.as_view(), views.TaskDetailView.as_view(),
name="task_detail", name="task_detail",
), ),
path("tasks/auth/todoist/", views.todoist_oauth, name="task_todoist_auth"),
] ]

View File

@ -1,6 +1,19 @@
import logging
from django.contrib import messages
from django.http import HttpResponseRedirect
from django.urls import reverse_lazy
from django.views.decorators.csrf import csrf_exempt
from rest_framework import status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from scrobbles.views import ScrobbleableDetailView, ScrobbleableListView
from tasks.models import Task from tasks.models import Task
from scrobbles.views import ScrobbleableListView, ScrobbleableDetailView from vrobbler.apps.tasks.todoist import get_todoist_access_token
logger = logging.getLogger(__name__)
class TaskListView(ScrobbleableListView): class TaskListView(ScrobbleableListView):
@ -9,3 +22,30 @@ class TaskListView(ScrobbleableListView):
class TaskDetailView(ScrobbleableDetailView): class TaskDetailView(ScrobbleableDetailView):
model = Task model = Task
@csrf_exempt
@permission_classes([IsAuthenticated])
@api_view(["GET"])
def todoist_oauth(request):
logger.info(
"[todoist_oauth] called",
extra={"user_id": request.user.id, "get_data": request.GET},
)
get_todoist_access_token(
request.user.id, request.GET.get("state"), request.GET.get("code")
)
logger.info(
"[todoist_oauth] finished",
extra={"user_id": request.user.id},
)
messages.add_message(
request,
messages.SUCCESS,
f"Todoist successfully configured",
)
success_url = reverse_lazy("vrobbler-home")
return HttpResponseRedirect(success_url)

View File

@ -0,0 +1,39 @@
import logging
from django.views.decorators.csrf import csrf_exempt
from rest_framework import status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
logger = logging.getLogger(__name__)
@csrf_exempt
@permission_classes([IsAuthenticated])
@api_view(["POST"])
def todoist_webhook(request):
post_data = request.data
logger.info(
"[todoist_webhook] called",
extra={"post_data": post_data},
)
# Disregard progress updates
if in_progress and is_music:
logger.info(
"[jellyfin_webhook] ignoring update of music in progress",
extra={"post_data": post_data},
)
return Response({}, status=status.HTTP_304_NOT_MODIFIED)
scrobble = todoist_scrobble_task(post_data, request.user.id)
if not scrobble:
return Response({}, status=status.HTTP_400_BAD_REQUEST)
logger.info(
"[jellyfin_webhook] finished",
extra={"scrobble_id": scrobble.id},
)
return Response({"scrobble_id": scrobble.id}, status=status.HTTP_200_OK)

View File

@ -71,6 +71,8 @@ GEOLOC_PROXIMITY = os.getenv("VROBBLER_GEOLOC_PROXIMITY", "0.0001")
POINTS_FOR_MOVEMENT_HISTORY = os.getenv( POINTS_FOR_MOVEMENT_HISTORY = os.getenv(
"VROBBLER_POINTS_FOR_MOVEMENT_HISTORY", 3 "VROBBLER_POINTS_FOR_MOVEMENT_HISTORY", 3
) )
TODOIST_CLIENT_ID = os.getenv("VROBBLER_TODOIST_CLIENT_ID", "")
TODOIST_CLIENT_SECRET = os.getenv("VROBBLER_TODOIST_CLIENT_SECRET", "")
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"